Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/pkg/omnictl/cluster/template/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func deleteImpl(ctx context.Context, client *client.Client, _ access.ServerInfo)

defer f.Close() //nolint:errcheck

return operations.DeleteTemplate(ctx, f, os.Stdout, client.Omni().State(), deleteCmdFlags.options)
return operations.DeleteTemplate(ctx, f, os.Stdout, client.Omni().State(), deleteCmdFlags.options, resolvedRoot)
}

func init() {
Expand Down
2 changes: 1 addition & 1 deletion client/pkg/omnictl/cluster/template/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func diff(ctx context.Context, client *client.Client, _ access.ServerInfo) error

defer f.Close() //nolint:errcheck

return operations.DiffTemplate(ctx, f, os.Stdout, client.Omni().State())
return operations.DiffTemplate(ctx, f, os.Stdout, client.Omni().State(), resolvedRoot)
}

func init() {
Expand Down
2 changes: 1 addition & 1 deletion client/pkg/omnictl/cluster/template/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func render() error {

defer f.Close() //nolint:errcheck

return operations.RenderTemplate(f, os.Stdout)
return operations.RenderTemplate(f, os.Stdout, resolvedRoot)
}

func init() {
Expand Down
2 changes: 1 addition & 1 deletion client/pkg/omnictl/cluster/template/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func status(ctx context.Context, client *client.Client, _ access.ServerInfo) err
statusCmdFlags.options.Wait = false
}

return operations.StatusTemplate(ctx, f, os.Stdout, client.Omni().State(), statusCmdFlags.options)
return operations.StatusTemplate(ctx, f, os.Stdout, client.Omni().State(), statusCmdFlags.options, resolvedRoot)
}

func init() {
Expand Down
2 changes: 1 addition & 1 deletion client/pkg/omnictl/cluster/template/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func syncTemplateFiles(ctx context.Context, client *client.Client, _ access.Serv
return fmt.Errorf("failed to open template file %q: %w", file, err)
}

err = operations.SyncTemplate(ctx, f, os.Stdout, client.Omni().State(), syncCmdFlags.options)
err = operations.SyncTemplate(ctx, f, os.Stdout, client.Omni().State(), syncCmdFlags.options, resolvedRoot)
f.Close() //nolint:errcheck

if err != nil {
Expand Down
30 changes: 30 additions & 0 deletions client/pkg/omnictl/cluster/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
package template

import (
"os"
"path/filepath"

"github.com/siderolabs/gen/ensure"
"github.com/spf13/cobra"
)
Expand All @@ -14,23 +17,50 @@ import (
var cmdFlags struct {
// Path to the cluster template file.
TemplatePath string
// AllowedDir is the directory that restricts file access in the template.
AllowedDir string
}

// resolvedRoot is an *os.Root opened at the root dir, resolved relative to the template file's directory.
// It is populated by PersistentPreRunE on templateCmd before any subcommand runs.
var resolvedRoot *os.Root

// templateCmd represents the template sub-command.
var templateCmd = &cobra.Command{
Use: "template",
Aliases: []string{"t"},
Short: "Cluster template management subcommands.",
Long: `Commands to render, validate, manage cluster templates.`,
Example: "",
PersistentPreRunE: func(*cobra.Command, []string) error {
templateDir := filepath.Dir(cmdFlags.TemplatePath)

p := cmdFlags.AllowedDir
if !filepath.IsAbs(p) {
p = filepath.Join(templateDir, p)
}

var err error

resolvedRoot, err = os.OpenRoot(p)
if err != nil {
return err
}

return nil
},
}

// RootCmd exports templateCmd.
func RootCmd() *cobra.Command {
templateCmd.PersistentFlags().StringVar(&cmdFlags.AllowedDir, "allowed-dir", "./", "allowed directory for file access in the template;"+
" relative paths are resolved against the template file's directory.")

return templateCmd
}

func addRequiredFileFlag(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(&cmdFlags.TemplatePath, "file", "f", "", "path to the cluster template file or directory.")

ensure.NoError(cmd.MarkPersistentFlagRequired("file"))
}
2 changes: 1 addition & 1 deletion client/pkg/omnictl/cluster/template/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func validate() error {

defer f.Close() //nolint:errcheck

return operations.ValidateTemplate(f)
return operations.ValidateTemplate(f, resolvedRoot)
}

func init() {
Expand Down
10 changes: 5 additions & 5 deletions client/pkg/template/internal/models/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ type TalosCluster struct {
}

// Validate the model.
func (cluster *Cluster) Validate() error {
func (cluster *Cluster) Validate(opts ValidateOptions) error {
var multiErr error

validator := omni.ClusterValidator{
Expand All @@ -101,11 +101,11 @@ func (cluster *Cluster) Validate() error {
multiErr = multierror.Append(multiErr, err)
}

if err := cluster.Patches.Validate(); err != nil {
if err := cluster.Patches.Validate(opts); err != nil {
multiErr = multierror.Append(multiErr, err)
}

if err := cluster.Kubernetes.Manifests.Validate(); err != nil {
if err := cluster.Kubernetes.Manifests.Validate(opts); err != nil {
multiErr = multierror.Append(multiErr, err)
}

Expand All @@ -132,12 +132,12 @@ func (cluster *Cluster) Translate(ctx TranslateContext) ([]resource.Resource, er
clusterResource.TypedSpec().Value.KubernetesVersion = strings.TrimLeft(cluster.Kubernetes.Version, "v")
clusterResource.TypedSpec().Value.TalosVersion = strings.TrimLeft(cluster.Talos.Version, "v")

patches, err := cluster.Patches.Translate(fmt.Sprintf("cluster-%s", cluster.Name), constants.PatchBaseWeightCluster, pair.MakePair(omni.LabelCluster, cluster.Name))
patches, err := cluster.Patches.Translate(ctx, fmt.Sprintf("cluster-%s", cluster.Name), constants.PatchBaseWeightCluster, pair.MakePair(omni.LabelCluster, cluster.Name))
if err != nil {
return nil, err
}

manifests, err := cluster.Kubernetes.Manifests.Translate(fmt.Sprintf("cluster-%s", cluster.Name), constants.PatchBaseWeightCluster,
manifests, err := cluster.Kubernetes.Manifests.Translate(ctx, fmt.Sprintf("cluster-%s", cluster.Name), constants.PatchBaseWeightCluster,
pair.MakePair(omni.LabelCluster, cluster.Name),
)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions client/pkg/template/internal/models/controlplane.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type ControlPlane struct {
}

// Validate the model.
func (controlplane *ControlPlane) Validate() error {
func (controlplane *ControlPlane) Validate(opts ValidateOptions) error {
var multiErr error

if controlplane.Name != "" {
Expand All @@ -47,7 +47,7 @@ func (controlplane *ControlPlane) Validate() error {
multiErr = multierror.Append(multiErr, fmt.Errorf("deleteStrategy is not allowed in the controlplane"))
}

multiErr = joinErrors(multiErr, controlplane.MachineSet.Validate(), controlplane.Machines.Validate(), controlplane.Patches.Validate())
multiErr = joinErrors(multiErr, controlplane.MachineSet.Validate(), controlplane.Machines.Validate(), controlplane.Patches.Validate(opts))
if multiErr != nil {
return fmt.Errorf("controlplane is invalid: %w", multiErr)
}
Expand Down
17 changes: 8 additions & 9 deletions client/pkg/template/internal/models/kubernetes_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"bytes"
"errors"
"fmt"
"os"

"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/kvutils"
Expand Down Expand Up @@ -65,7 +64,7 @@ type KubernetesManifest struct {
}

// Validate a kubernetes manifest.
func (km *KubernetesManifest) Validate() error {
func (km *KubernetesManifest) Validate(opts ValidateOptions) error {
var errs error

name := km.Name
Expand Down Expand Up @@ -95,7 +94,7 @@ func (km *KubernetesManifest) Validate() error {

switch {
case km.File != "":
_, err := os.Stat(km.File)
_, err := StatFile(opts.Root, km.File)
if err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to access %q: %w", km.File, err))
}
Expand All @@ -110,7 +109,7 @@ func (km *KubernetesManifest) Validate() error {
}

// Translate the model into a resource.
func (km *KubernetesManifest) Translate(prefix string, weight int, labels ...pair.Pair[string, string]) (*omni.KubernetesManifestGroup, error) {
func (km *KubernetesManifest) Translate(ctx TranslateContext, prefix string, weight int, labels ...pair.Pair[string, string]) (*omni.KubernetesManifestGroup, error) {
name := km.Name
if name == "" {
name = km.File
Expand All @@ -125,7 +124,7 @@ func (km *KubernetesManifest) Translate(prefix string, weight int, labels ...pai

switch {
case km.File != "":
raw, err = os.ReadFile(km.File)
raw, err = ReadFile(ctx.Root, km.File)
case km.Inline != nil:
var buf bytes.Buffer

Expand Down Expand Up @@ -172,11 +171,11 @@ func (km *KubernetesManifest) Translate(prefix string, weight int, labels ...pai
type KubernetesManifestsList []KubernetesManifest

// Validate the manifests in the list.
func (k KubernetesManifestsList) Validate() error {
func (k KubernetesManifestsList) Validate(opts ValidateOptions) error {
names := make(map[string]struct{}, len(k))

return errors.Join(xslices.Map(k, func(m KubernetesManifest) error {
if err := m.Validate(); err != nil {
if err := m.Validate(opts); err != nil {
return err
}

Expand All @@ -196,11 +195,11 @@ func (k KubernetesManifestsList) Validate() error {
}

// Translate the list of KubernetesManifests into a list of resources.
func (l KubernetesManifestsList) Translate(prefix string, baseWeight int, labels ...pair.Pair[string, string]) ([]resource.Resource, error) {
func (l KubernetesManifestsList) Translate(ctx TranslateContext, prefix string, baseWeight int, labels ...pair.Pair[string, string]) ([]resource.Resource, error) {
resources := make([]resource.Resource, 0, len(l))

for i, manifest := range l {
r, err := manifest.Translate(prefix, baseWeight+i, labels...)
r, err := manifest.Translate(ctx, prefix, baseWeight+i, labels...)
if err != nil {
return nil, err
}
Expand Down
8 changes: 5 additions & 3 deletions client/pkg/template/internal/models/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package models

import (
"fmt"
"os"
"slices"

"github.com/cosi-project/runtime/pkg/resource"
Expand All @@ -24,11 +25,11 @@ type List []Model
// Each model should be valid, but also the set of models should be complete.
//
//nolint:gocyclo,cyclop
func (l List) Validate() error {
func (l List) Validate(opts ValidateOptions) error {
var multiErr error

for _, model := range l {
multiErr = joinErrors(multiErr, model.Validate())
multiErr = joinErrors(multiErr, model.Validate(opts))
}

// complete template should contain 1 cluster, 1 controlplane, 0-N workers
Expand Down Expand Up @@ -121,11 +122,12 @@ func (l List) Validate() error {
// Translate a set of models (template) to a set of Omni resources.
//
// Translate assumes that the template is valid.
func (l List) Translate() ([]resource.Resource, error) {
func (l List) Translate(root *os.Root) ([]resource.Resource, error) {
context := TranslateContext{
LockedMachines: map[MachineID]struct{}{},
MachineDescriptors: map[MachineID]Descriptors{},
MachineSetLevelKernelArgs: map[MachineID]KernelArgs{},
Root: root,
}

for _, model := range l {
Expand Down
6 changes: 4 additions & 2 deletions client/pkg/template/internal/models/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (install *MachineInstall) Validate() error {
}

// Validate the model.
func (machine *Machine) Validate() error {
func (machine *Machine) Validate(opts ValidateOptions) error {
var multiErr error

if machine.Name == "" {
Expand All @@ -92,7 +92,7 @@ func (machine *Machine) Validate() error {
multiErr = multierror.Append(multiErr, err)
}

multiErr = joinErrors(multiErr, machine.Name.Validate(), machine.Install.Validate(), machine.Patches.Validate())
multiErr = joinErrors(multiErr, machine.Name.Validate(), machine.Install.Validate(), machine.Patches.Validate(opts))
if multiErr != nil {
return fmt.Errorf("machine %q is invalid: %w", machine.Name, multiErr)
}
Expand All @@ -117,6 +117,7 @@ func (machine *Machine) Translate(ctx TranslateContext) ([]resource.Resource, er
}

patchResource, err := patch.Translate(
ctx,
fmt.Sprintf("cm-%s", machine.Name),
constants.PatchWeightInstallDisk,
pair.MakePair(omni.LabelCluster, ctx.ClusterName),
Expand All @@ -131,6 +132,7 @@ func (machine *Machine) Translate(ctx TranslateContext) ([]resource.Resource, er
}

patches, err := machine.Patches.Translate(
ctx,
fmt.Sprintf("cm-%s", machine.Name),
constants.PatchBaseWeightClusterMachine,
pair.MakePair(omni.LabelCluster, ctx.ClusterName),
Expand Down
1 change: 1 addition & 0 deletions client/pkg/template/internal/models/machineset.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ func (machineset *MachineSet) translate(ctx TranslateContext, nameSuffix, roleLa
}

patches, err := machineset.Patches.Translate(
ctx,
id,
constants.PatchBaseWeightMachineSet,
pair.MakePair(omni.LabelCluster, ctx.ClusterName),
Expand Down
55 changes: 54 additions & 1 deletion client/pkg/template/internal/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package models

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/cosi-project/runtime/pkg/resource"
Expand All @@ -24,6 +26,8 @@ type Meta struct {

// TranslateContext is a context for translation.
type TranslateContext struct {
// Root restricts file access to a single directory tree. When nil, no restriction is applied.
Root *os.Root
LockedMachines map[MachineID]struct{}
MachineDescriptors map[MachineID]Descriptors
MachineSetLevelKernelArgs map[MachineID]KernelArgs
Expand Down Expand Up @@ -97,9 +101,58 @@ func (d *Descriptors) Apply(res resource.Resource) {
}
}

// ValidateOptions contains options for model validation.
type ValidateOptions struct {
// Root restricts file access to a single directory tree. When nil, no restriction is applied.
Root *os.Root
}

// resolveForRoot translates a CWD-relative path into a path relative to the root directory.
func resolveForRoot(root *os.Root, path string) (string, error) {
absPath, err := filepath.Abs(path)
if err != nil {
return "", err
}

rootAbs, err := filepath.Abs(root.Name())
if err != nil {
return "", err
}

return filepath.Rel(rootAbs, absPath)
}

// ReadFile reads a file, using root to restrict access when non-nil.
func ReadFile(root *os.Root, path string) ([]byte, error) {
if root == nil {
return os.ReadFile(path)
}

rel, err := resolveForRoot(root, path)
if err != nil {
return nil, err
}

return root.ReadFile(rel)
}

// StatFile stats a file, using root to restrict access when non-nil.
func StatFile(root *os.Root, path string) (os.FileInfo, error) {
if root == nil {
return os.Stat(path)
}

rel, err := resolveForRoot(root, path)
if err != nil {
return nil, err
}

return root.Stat(rel)
}

// Model is a base interface for cluster templates.
type Model interface {
Validate() error
Validate(ValidateOptions) error
Translate(TranslateContext) ([]resource.Resource, error)
}

Expand Down
Loading
Loading