diff --git a/.golangci.json b/.golangci.json index a923484e83..6455fbc9b3 100644 --- a/.golangci.json +++ b/.golangci.json @@ -27,12 +27,9 @@ "disable-all": true, "enable": [ "misspell", - "structcheck", "govet", "staticcheck", - "deadcode", "errcheck", - "varcheck", "unparam", "ineffassign", "nakedret", diff --git a/cmd/fleetagent/main.go b/cmd/fleetagent/main.go index da1654e933..ceaf4a9947 100644 --- a/cmd/fleetagent/main.go +++ b/cmd/fleetagent/main.go @@ -31,7 +31,7 @@ type FleetAgent struct { func (a *FleetAgent) Run(cmd *cobra.Command, args []string) error { go func() { - log.Println(http.ListenAndServe("localhost:6060", nil)) + log.Println(http.ListenAndServe("localhost:6060", nil)) // nolint:gosec // Debugging only }() debugConfig.MustSetupDebug() diff --git a/cmd/fleetcontroller/main.go b/cmd/fleetcontroller/main.go index d0c1d5ab24..6c77cc9a5a 100644 --- a/cmd/fleetcontroller/main.go +++ b/cmd/fleetcontroller/main.go @@ -29,7 +29,7 @@ type FleetManager struct { func (f *FleetManager) Run(cmd *cobra.Command, args []string) error { go func() { - log.Println(http.ListenAndServe("localhost:6060", nil)) + log.Println(http.ListenAndServe("localhost:6060", nil)) // nolint:gosec // Debugging only }() debugConfig.MustSetupDebug() if err := fleetcontroller.Start(cmd.Context(), f.Namespace, f.Kubeconfig, f.DisableGitops); err != nil { diff --git a/dev/build-fleet b/dev/build-fleet index d049b92d35..3a6715e5d1 100755 --- a/dev/build-fleet +++ b/dev/build-fleet @@ -8,7 +8,6 @@ if [ ! -d ./cmd/fleetcontroller ]; then exit 1 fi -export GOOS=linux export GOARCH="${GOARCH:-amd64}" export CGO_ENABLED=0 @@ -17,6 +16,7 @@ if ! git diff --quiet HEAD origin/master -- pkg/apis/fleet.cattle.io/v1alpha1; go generate fi +export GOOS=linux # fleet go build -gcflags='all=-N -l' -o bin/fleetcontroller-linux-"$GOARCH" ./cmd/fleetcontroller docker build -f package/Dockerfile -t rancher/fleet:dev --build-arg="ARCH=$GOARCH" . diff --git a/modules/agent/pkg/deployer/manager.go b/modules/agent/pkg/deployer/manager.go index fcca262680..6ae2817bd5 100644 --- a/modules/agent/pkg/deployer/manager.go +++ b/modules/agent/pkg/deployer/manager.go @@ -121,7 +121,8 @@ func (m *Manager) Resources(bd *fleet.BundleDeployment) (*helmdeployer.Resources return resources, nil } -// Deploy the bundle deployment, i.e. with helmdeployer +// Deploy the bundle deployment, i.e. with helmdeployer. +// This loads the manifest and the contents from the upstream cluster. func (m *Manager) Deploy(bd *fleet.BundleDeployment) (string, error) { if bd.Spec.DeploymentID == bd.Status.AppliedDeploymentID { if ok, err := m.deployer.EnsureInstalled(bd.Name, bd.Status.Release); err != nil { diff --git a/modules/agent/pkg/register/register.go b/modules/agent/pkg/register/register.go index 7a5cb24f37..5d26fbb876 100644 --- a/modules/agent/pkg/register/register.go +++ b/modules/agent/pkg/register/register.go @@ -31,7 +31,6 @@ import ( const ( CredName = "fleet-agent" - BootstrapCredName = "fleet-agent-bootstrap" // same as config.AgentBootstrapConfigName for fleet-controller Kubeconfig = "kubeconfig" Token = "token" Values = "values" @@ -65,10 +64,10 @@ func Register(ctx context.Context, namespace, clusterID string, config *rest.Con // tryRegister makes sure the secret cattle-fleet-system/fleet-agent is // populated and the contained kubeconfig is working -func tryRegister(ctx context.Context, namespace, clusterID string, config *rest.Config) (*AgentInfo, error) { - config = rest.CopyConfig(config) - config.RateLimiter = ratelimit.None - k8s, err := core.NewFactoryFromConfig(config) +func tryRegister(ctx context.Context, namespace, clusterID string, cfg *rest.Config) (*AgentInfo, error) { + cfg = rest.CopyConfig(cfg) + cfg.RateLimiter = ratelimit.None + k8s, err := core.NewFactoryFromConfig(cfg) if err != nil { return nil, err } @@ -78,7 +77,7 @@ func tryRegister(ctx context.Context, namespace, clusterID string, config *rest. // fallback to local cattle-fleet-system/fleet-agent-bootstrap secret, err = runRegistration(ctx, k8s.Core().V1(), namespace, clusterID) if err != nil { - return nil, fmt.Errorf("looking up secret %s/%s: %w", namespace, BootstrapCredName, err) + return nil, fmt.Errorf("looking up secret %s/%s: %w", namespace, config.AgentBootstrapConfigName, err) } } else if err != nil { return nil, err @@ -87,7 +86,7 @@ func tryRegister(ctx context.Context, namespace, clusterID string, config *rest. logrus.Errorf("Current credential failed, failing back to reregistering: %v", err) secret, err = runRegistration(ctx, k8s.Core().V1(), namespace, clusterID) if err != nil { - return nil, fmt.Errorf("looking up secret %s/%s or %s/%s: %w", namespace, BootstrapCredName, namespace, CredName, err) + return nil, fmt.Errorf("looking up secret %s/%s or %s/%s: %w", namespace, config.AgentBootstrapConfigName, namespace, CredName, err) } } @@ -97,7 +96,7 @@ func tryRegister(ctx context.Context, namespace, clusterID string, config *rest. } // delete the bootstrap cred - _ = k8s.Core().V1().Secret().Delete(namespace, BootstrapCredName, nil) + _ = k8s.Core().V1().Secret().Delete(namespace, config.AgentBootstrapConfigName, nil) return &AgentInfo{ ClusterNamespace: string(secret.Data[ClusterNamespace]), ClusterName: string(secret.Data[ClusterName]), @@ -107,9 +106,9 @@ func tryRegister(ctx context.Context, namespace, clusterID string, config *rest. func runRegistration(ctx context.Context, k8s corecontrollers.Interface, namespace, clusterID string) (*corev1.Secret, error) { // read cattle-fleet-system/fleet-agent-bootstrap - secret, err := k8s.Secret().Get(namespace, BootstrapCredName, metav1.GetOptions{}) + secret, err := k8s.Secret().Get(namespace, config.AgentBootstrapConfigName, metav1.GetOptions{}) if err != nil { - return nil, fmt.Errorf("looking up secret %s/%s: %w", namespace, BootstrapCredName, err) + return nil, fmt.Errorf("looking up secret %s/%s: %w", namespace, config.AgentBootstrapConfigName, err) } return createClusterSecret(ctx, clusterID, k8s, secret) } diff --git a/modules/agent/pkg/simulator/simulator.go b/modules/agent/pkg/simulator/simulator.go index 1536e99407..262a2d377d 100644 --- a/modules/agent/pkg/simulator/simulator.go +++ b/modules/agent/pkg/simulator/simulator.go @@ -103,7 +103,7 @@ func setupNamespace(ctx context.Context, kubeConfig, namespace, simNamespace str clusterID := name.SafeConcatName(simNamespace, strings.SplitN(string(kubeSystem.UID), "-", 2)[0]) if _, err = k8s.CoreV1().Secrets(simNamespace).Get(ctx, register.CredName, metav1.GetOptions{}); err != nil { - secret, err := k8s.CoreV1().Secrets(namespace).Get(ctx, register.BootstrapCredName, metav1.GetOptions{}) + secret, err := k8s.CoreV1().Secrets(namespace).Get(ctx, config.AgentBootstrapConfigName, metav1.GetOptions{}) if err != nil { return "", err } diff --git a/modules/cli/apply/apply.go b/modules/cli/apply/apply.go index 4f60265249..bf5c8b8ca8 100644 --- a/modules/cli/apply/apply.go +++ b/modules/cli/apply/apply.go @@ -16,8 +16,8 @@ import ( "github.com/rancher/fleet/modules/cli/pkg/client" fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" - "github.com/rancher/fleet/pkg/bundle" - "github.com/rancher/fleet/pkg/bundleyaml" + "github.com/rancher/fleet/pkg/bundlereader" + "github.com/rancher/fleet/pkg/fleetyaml" name2 "github.com/rancher/wrangler/pkg/name" "github.com/rancher/wrangler/pkg/yaml" @@ -47,7 +47,7 @@ type Options struct { Paused bool Labels map[string]string SyncGeneration int64 - Auth bundle.Auth + Auth bundlereader.Auth } func globDirs(baseDir string) (result []string, err error) { @@ -97,7 +97,7 @@ func Apply(ctx context.Context, client *client.Getter, repoName string, baseDirs if !info.IsDir() { return nil } - if !bundleyaml.FoundFleetYamlInDirectory(path) { + if !fleetyaml.FoundFleetYamlInDirectory(path) { return nil } } @@ -155,18 +155,18 @@ func pruneBundlesNotFoundInRepo(client *client.Getter, repoName string, gitRepoB return err } -// readBundle reads bundle data from a source and return a bundle with the +// readBundle reads bundle data from a source and returns a bundle with the // given name, or the name from the raw source file -func readBundle(ctx context.Context, name, baseDir string, opts *Options) (*bundle.Bundle, error) { +func readBundle(ctx context.Context, name, baseDir string, opts *Options) (*fleet.Bundle, []*fleet.ImageScan, error) { if opts.BundleReader != nil { - var bundleResource fleet.Bundle - if err := json.NewDecoder(opts.BundleReader).Decode(&bundleResource); err != nil { - return nil, err + var bundle *fleet.Bundle + if err := json.NewDecoder(opts.BundleReader).Decode(bundle); err != nil { + return nil, nil, err } - return bundle.New(&bundleResource) + return bundle, nil, nil } - return bundle.Open(ctx, name, baseDir, opts.BundleFile, &bundle.Options{ + return bundlereader.Open(ctx, name, baseDir, opts.BundleFile, &bundlereader.Options{ Compress: opts.Compress, Labels: opts.Labels, ServiceAccount: opts.ServiceAccount, @@ -228,12 +228,12 @@ func Dir(ctx context.Context, client *client.Getter, name, baseDir string, opts if opts == nil { opts = &Options{} } - bundle, err := readBundle(ctx, createName(name, baseDir), baseDir, opts) + bundle, scans, err := readBundle(ctx, createName(name, baseDir), baseDir, opts) if err != nil { return err } - def := bundle.Definition.DeepCopy() + def := bundle.DeepCopy() def.Namespace = client.Namespace if len(def.Spec.Resources) == 0 { @@ -242,7 +242,7 @@ func Dir(ctx context.Context, client *client.Getter, name, baseDir string, opts gitRepoBundlesMap[def.Name] = true objects := []runtime.Object{def} - for _, scan := range bundle.Scans { + for _, scan := range scans { objects = append(objects, scan) } @@ -252,7 +252,7 @@ func Dir(ctx context.Context, client *client.Getter, name, baseDir string, opts } if opts.Output == nil { - err = save(client, def, bundle.Scans...) + err = save(client, def, scans...) } else { _, err = opts.Output.Write(b) } diff --git a/modules/cli/match/match.go b/modules/cli/match/match.go index 4a2103aa56..8366eb2d8b 100644 --- a/modules/cli/match/match.go +++ b/modules/cli/match/match.go @@ -1,4 +1,7 @@ // Package match is used to test matching a bundles to a target on the command line. (fleetapply) +// +// It's not used by fleet, but it is available in the fleet CLI as "test" sub +// command. The tests in fleet-examples use it. package match import ( @@ -10,7 +13,8 @@ import ( "os" fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" - "github.com/rancher/fleet/pkg/bundle" + "github.com/rancher/fleet/pkg/bundlematcher" + "github.com/rancher/fleet/pkg/bundlereader" "github.com/rancher/fleet/pkg/helmdeployer" "github.com/rancher/fleet/pkg/manifest" "github.com/rancher/fleet/pkg/options" @@ -36,12 +40,12 @@ func Match(ctx context.Context, opts *Options) error { } var ( - b *bundle.Bundle - err error + bundle *fleet.Bundle + err error ) if opts.BundleFile == "" { - b, err = bundle.Open(ctx, "test", opts.BaseDir, opts.BundleSpec, nil) + bundle, _, err = bundlereader.Open(ctx, "test", opts.BaseDir, opts.BundleSpec, nil) if err != nil { return err } @@ -51,44 +55,44 @@ func Match(ctx context.Context, opts *Options) error { return err } - bundleConfig := &fleet.Bundle{} - if err := yaml.Unmarshal(data, bundleConfig); err != nil { + bundle = &fleet.Bundle{} + if err := yaml.Unmarshal(data, bundle); err != nil { return err } + } - b, err = bundle.New(bundleConfig) - if err != nil { - return err - } + bm, err := bundlematcher.New(bundle) + if err != nil { + return err } if opts.Target == "" { - m := b.Match(opts.ClusterName, map[string]map[string]string{ + m := bm.Match(opts.ClusterName, map[string]map[string]string{ opts.ClusterGroup: opts.ClusterGroupLabels, }, opts.ClusterLabels) - return printMatch(b, m, opts.Output) + return printMatch(bundle, m, opts.Output) } - return printMatch(b, b.MatchForTarget(opts.Target), opts.Output) + return printMatch(bundle, bm.MatchForTarget(opts.Target), opts.Output) } -func printMatch(bundle *bundle.Bundle, m *bundle.Match, output io.Writer) error { - if m == nil { +func printMatch(bundle *fleet.Bundle, target *fleet.BundleTarget, output io.Writer) error { + if target == nil { return errors.New("no match found") } - fmt.Fprintf(os.Stderr, "# Matched: %s\n", m.Target.Name) + fmt.Fprintf(os.Stderr, "# Matched: %s\n", target.Name) if output == nil { return nil } - opts := options.Calculate(&bundle.Definition.Spec, m.Target) + opts := options.Merge(bundle.Spec.BundleDeploymentOptions, target.BundleDeploymentOptions) - manifest, err := manifest.New(&bundle.Definition.Spec) + manifest, err := manifest.New(bundle.Spec.Resources) if err != nil { return err } - objs, err := helmdeployer.Template(m.Bundle.Definition.Name, manifest, opts) + objs, err := helmdeployer.Template(bundle.Name, manifest, opts) if err != nil { return err } diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index f2c86368db..248c31d531 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -147,7 +147,7 @@ func getToken(ctx context.Context, controllerNamespace, tokenName string, client return nil, fmt.Errorf("failed to find token in values") } - expectedNamespace := fleetns.RegistrationNamespace(controllerNamespace) + expectedNamespace := fleetns.SystemRegistrationNamespace(controllerNamespace) actualNamespace := data["systemRegistrationNamespace"] if actualNamespace != expectedNamespace { return nil, fmt.Errorf("registration namespace (%s) from secret (%s/%s) does not match expected: %s", actualNamespace, secret.Namespace, secret.Name, expectedNamespace) diff --git a/pkg/agent/config.go b/pkg/agent/config.go index 582dcb4575..dc54549c21 100644 --- a/pkg/agent/config.go +++ b/pkg/agent/config.go @@ -4,9 +4,10 @@ import ( "context" "github.com/rancher/fleet/modules/cli/pkg/client" - "github.com/rancher/fleet/pkg/basic" "github.com/rancher/fleet/pkg/config" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -44,7 +45,11 @@ func configObjects(controllerNamespace string, clusterLabels map[string]string, } cm.Name = "fleet-agent" return []runtime.Object{ - basic.Namespace(controllerNamespace), + &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: controllerNamespace, + }, + }, cm, }, nil } diff --git a/pkg/agent/manifest.go b/pkg/agent/manifest.go index 7a6683ca6d..79fbefeaf9 100644 --- a/pkg/agent/manifest.go +++ b/pkg/agent/manifest.go @@ -7,10 +7,10 @@ import ( "github.com/sirupsen/logrus" - "github.com/rancher/fleet/pkg/basic" "github.com/rancher/fleet/pkg/config" "github.com/rancher/wrangler/pkg/name" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -45,11 +45,11 @@ func Manifest(namespace string, agentScope string, opts ManifestOptions) []runti opts.AgentImage = config.DefaultAgentImage } - sa := basic.ServiceAccount(namespace, DefaultName) + sa := serviceAccount(namespace, DefaultName) logrus.Debugf("Building manifest for fleet-agent in namespace %s (sa: %s)", namespace, sa.Name) - defaultSa := basic.ServiceAccount(namespace, "default") + defaultSa := serviceAccount(namespace, "default") defaultSa.AutomountServiceAccountToken = new(bool) clusterRole := []runtime.Object{ @@ -90,7 +90,7 @@ func Manifest(namespace string, agentScope string, opts ManifestOptions) []runti // if debug is enabled in controller, enable in agent too debug := logrus.IsLevelEnabled(logrus.DebugLevel) - dep := basic.Deployment(namespace, DefaultName, image, opts.AgentImagePullPolicy, DefaultName, false, debug) + dep := agentDeployment(namespace, DefaultName, image, opts.AgentImagePullPolicy, DefaultName, false, debug) dep.Spec.Template.Spec.Containers[0].Env = append(dep.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{ Name: "AGENT_SCOPE", @@ -171,3 +171,83 @@ func resolve(global, prefix, image string) string { return image } + +func agentDeployment(namespace, name, image, imagePullPolicy, serviceAccount string, linuxOnly, debug bool) *appsv1.Deployment { + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": name, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": name, + }, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: serviceAccount, + Containers: []corev1.Container{ + { + Name: name, + Image: image, + ImagePullPolicy: corev1.PullPolicy(imagePullPolicy), + Env: []corev1.EnvVar{ + { + Name: "NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + if !debug { + for _, container := range deployment.Spec.Template.Spec.Containers { + container.SecurityContext = &corev1.SecurityContext{ + AllowPrivilegeEscalation: &[]bool{false}[0], + ReadOnlyRootFilesystem: &[]bool{true}[0], + } + } + deployment.Spec.Template.Spec.SecurityContext = &corev1.PodSecurityContext{ + RunAsNonRoot: &[]bool{true}[0], + RunAsUser: &[]int64{1000}[0], + RunAsGroup: &[]int64{1000}[0], + } + } + if linuxOnly { + deployment.Spec.Template.Spec.NodeSelector = map[string]string{"kubernetes.io/os": "linux"} + } + deployment.Spec.Template.Spec.Tolerations = append(deployment.Spec.Template.Spec.Tolerations, corev1.Toleration{ + Key: "node.cloudprovider.kubernetes.io/uninitialized", + Operator: corev1.TolerationOpEqual, + Value: "true", + Effect: corev1.TaintEffectNoSchedule, + }, corev1.Toleration{ + Key: "cattle.io/os", + Operator: corev1.TolerationOpEqual, + Value: "linux", + Effect: corev1.TaintEffectNoSchedule, + }) + return deployment +} + +func serviceAccount(namespace, name string) *corev1.ServiceAccount { + return &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } +} diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/target.go b/pkg/apis/fleet.cattle.io/v1alpha1/target.go index ba4bfb96b9..88ce191b56 100644 --- a/pkg/apis/fleet.cattle.io/v1alpha1/target.go +++ b/pkg/apis/fleet.cattle.io/v1alpha1/target.go @@ -66,17 +66,24 @@ type ClusterSpec struct { KubeConfigSecret string `json:"kubeConfigSecret,omitempty"` RedeployAgentGeneration int64 `json:"redeployAgentGeneration,omitempty"` AgentEnvVars []v1.EnvVar `json:"agentEnvVars,omitempty"` - AgentNamespace string `json:"agentNamespace,omitempty"` - PrivateRepoURL string `json:"privateRepoURL,omitempty"` + + // AgentNamespace defaults to the system namespace, e.g. cattle-fleet-system + AgentNamespace string `json:"agentNamespace,omitempty"` + PrivateRepoURL string `json:"privateRepoURL,omitempty"` } type ClusterStatus struct { - Conditions []genericcondition.GenericCondition `json:"conditions,omitempty"` - Namespace string `json:"namespace,omitempty"` - Summary BundleSummary `json:"summary,omitempty"` - ResourceCounts GitRepoResourceCounts `json:"resourceCounts,omitempty"` - ReadyGitRepos int `json:"readyGitRepos"` - DesiredReadyGitRepos int `json:"desiredReadyGitRepos"` + Conditions []genericcondition.GenericCondition `json:"conditions,omitempty"` + + // Namespace is the cluster namespace, it contains the clusters service + // account as well as any bundledeployments. Example: + // "cluster-fleet-local-cluster-294db1acfa77-d9ccf852678f" + Namespace string `json:"namespace,omitempty"` + + Summary BundleSummary `json:"summary,omitempty"` + ResourceCounts GitRepoResourceCounts `json:"resourceCounts,omitempty"` + ReadyGitRepos int `json:"readyGitRepos"` + DesiredReadyGitRepos int `json:"desiredReadyGitRepos"` AgentEnvVarsHash string `json:"agentEnvVarsHash,omitempty"` AgentPrivateRepoURL string `json:"agentPrivateRepoURL,omitempty"` diff --git a/pkg/basic/objects.go b/pkg/basic/objects.go deleted file mode 100644 index 856481c67b..0000000000 --- a/pkg/basic/objects.go +++ /dev/null @@ -1,121 +0,0 @@ -// Package basic provides basic resources, like deployments, services, etc. (fleetcontroller) -package basic - -import ( - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func ConfigMap(namespace, name string, kvs ...string) *corev1.ConfigMap { - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Data: map[string]string{}, - } - - for i := range kvs { - if i%2 != 0 { - continue - } - v := "" - if len(kvs) > i { - v = kvs[i+1] - } - cm.Data[kvs[i]] = v - } - - return cm -} - -func Namespace(name string) *corev1.Namespace { - return &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - } - -} - -func Deployment(namespace, name, image, imagePullPolicy, serviceAccount string, linuxOnly, debug bool) *appsv1.Deployment { - deployment := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: name, - }, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": name, - }, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - "app": name, - }, - }, - Spec: corev1.PodSpec{ - ServiceAccountName: serviceAccount, - Containers: []corev1.Container{ - { - Name: name, - Image: image, - ImagePullPolicy: corev1.PullPolicy(imagePullPolicy), - Env: []corev1.EnvVar{ - { - Name: "NAMESPACE", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.namespace", - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - if !debug { - for _, container := range deployment.Spec.Template.Spec.Containers { - container.SecurityContext = &corev1.SecurityContext{ - AllowPrivilegeEscalation: &[]bool{false}[0], - ReadOnlyRootFilesystem: &[]bool{true}[0], - } - } - deployment.Spec.Template.Spec.SecurityContext = &corev1.PodSecurityContext{ - RunAsNonRoot: &[]bool{true}[0], - RunAsUser: &[]int64{1000}[0], - RunAsGroup: &[]int64{1000}[0], - } - } - if linuxOnly { - deployment.Spec.Template.Spec.NodeSelector = map[string]string{"kubernetes.io/os": "linux"} - } - deployment.Spec.Template.Spec.Tolerations = append(deployment.Spec.Template.Spec.Tolerations, corev1.Toleration{ - Key: "node.cloudprovider.kubernetes.io/uninitialized", - Operator: corev1.TolerationOpEqual, - Value: "true", - Effect: corev1.TaintEffectNoSchedule, - }, corev1.Toleration{ - Key: "cattle.io/os", - Operator: corev1.TolerationOpEqual, - Value: "linux", - Effect: corev1.TaintEffectNoSchedule, - }) - return deployment -} - -func ServiceAccount(namespace, name string) *corev1.ServiceAccount { - return &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - } -} - diff --git a/pkg/bundle/bundle.go b/pkg/bundle/bundle.go deleted file mode 100644 index 880e1b8711..0000000000 --- a/pkg/bundle/bundle.go +++ /dev/null @@ -1,20 +0,0 @@ -package bundle - -import ( - fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" -) - -// Bundle struct extends the fleet.Bundle with cluster matches and ImageScan configuration -type Bundle struct { - Definition *fleet.Bundle - Scans []*fleet.ImageScan - matcher *matcher -} - -func New(bundle *fleet.Bundle, imageScan ...*fleet.ImageScan) (*Bundle, error) { - a := &Bundle{ - Definition: bundle, - Scans: imageScan, - } - return a, a.initMatcher() -} diff --git a/pkg/bundle/resources.go b/pkg/bundle/resources.go deleted file mode 100644 index aa300b445e..0000000000 --- a/pkg/bundle/resources.go +++ /dev/null @@ -1,530 +0,0 @@ -package bundle - -import ( - "context" - "crypto/sha256" - "crypto/tls" - "crypto/x509" - "encoding/base64" - "fmt" - "io" - "net/http" - "net/url" - "os" - "path/filepath" - "regexp" - "sort" - "strings" - "sync" - "unicode/utf8" - - "github.com/hashicorp/go-getter" - "github.com/pkg/errors" - "golang.org/x/sync/errgroup" - "golang.org/x/sync/semaphore" - "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/downloader" - helmgetter "helm.sh/helm/v3/pkg/getter" - "helm.sh/helm/v3/pkg/registry" - "helm.sh/helm/v3/pkg/repo" - - "github.com/rancher/fleet/modules/cli/pkg/progress" - fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" - "github.com/rancher/fleet/pkg/content" - - "github.com/rancher/wrangler/pkg/data" - - "sigs.k8s.io/yaml" -) - -var hasOCIURL = regexp.MustCompile(`^oci:\/\/`) - -func readResources(ctx context.Context, spec *fleet.BundleSpec, compress bool, base string, auth Auth) ([]fleet.BundleResource, error) { - var directories []directory - - directories, err := addDirectory(directories, base, ".", ".") - if err != nil { - return nil, err - } - - var chartDirs []*fleet.HelmOptions - - if spec.Helm != nil && spec.Helm.Chart != "" { - if err := parseValueFiles(base, spec.Helm); err != nil { - return nil, err - } - chartDirs = append(chartDirs, spec.Helm) - } - - for _, target := range spec.Targets { - if target.Helm != nil { - err := parseValueFiles(base, target.Helm) - if err != nil { - return nil, err - } - if target.Helm.Chart != "" { - chartDirs = append(chartDirs, target.Helm) - } - } - } - - directories, err = addCharts(directories, base, chartDirs, auth) - if err != nil { - return nil, err - } - - resources, err := readDirectories(ctx, compress, directories...) - if err != nil { - return nil, err - } - - var result []fleet.BundleResource - for _, resources := range resources { - result = append(result, resources...) - } - - sort.Slice(result, func(i, j int) bool { - return result[i].Name < result[j].Name - }) - - return result, nil -} - -func checksum(helm *fleet.HelmOptions) string { - if helm == nil { - return "none" - } - return fmt.Sprintf(".chart/%x", sha256.Sum256([]byte(helm.Chart + ":" + helm.Repo + ":" + helm.Version)[:])) -} - -type Auth struct { - Username string - Password string - CABundle []byte - SSHPrivateKey []byte -} - -func chartURL(location *fleet.HelmOptions, auth Auth) (string, error) { - // repos are not supported in case of OCI Charts - if hasOCIURL.MatchString(location.Chart) { - return location.Chart, nil - } - - if location.Repo == "" { - return location.Chart, nil - } - - if !strings.HasSuffix(location.Repo, "/") { - location.Repo = location.Repo + "/" - } - - request, err := http.NewRequest("GET", location.Repo+"index.yaml", nil) - if err != nil { - return "", err - } - - if auth.Username != "" && auth.Password != "" { - request.SetBasicAuth(auth.Username, auth.Password) - } - client := &http.Client{} - if auth.CABundle != nil { - pool, err := x509.SystemCertPool() - if err != nil { - pool = x509.NewCertPool() - } - pool.AppendCertsFromPEM(auth.CABundle) - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.TLSClientConfig = &tls.Config{ - RootCAs: pool, - MinVersion: tls.VersionTLS12, - } - client.Transport = transport - } - - resp, err := client.Do(request) - if err != nil { - return "", err - } - defer resp.Body.Close() - - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - if resp.StatusCode != 200 { - return "", fmt.Errorf("failed to read helm repo from %s, error code: %v, response body: %s", location.Repo+"index.yaml", resp.StatusCode, bytes) - } - - repo := &repo.IndexFile{} - if err := yaml.Unmarshal(bytes, repo); err != nil { - return "", err - } - - repo.SortEntries() - - chart, err := repo.Get(location.Chart, location.Version) - if err != nil { - return "", err - } - - if len(chart.URLs) == 0 { - return "", fmt.Errorf("no URLs found for chart %s %s at %s", chart.Name, chart.Version, location.Repo) - } - - chartURL, err := url.Parse(chart.URLs[0]) - if err != nil { - return "", err - } - - if chartURL.IsAbs() { - return chart.URLs[0], nil - } - - repoURL, err := url.Parse(location.Repo) - if err != nil { - return "", err - } - - return repoURL.ResolveReference(chartURL).String(), nil -} - -func addCharts(directories []directory, base string, charts []*fleet.HelmOptions, auth Auth) ([]directory, error) { - for _, chart := range charts { - if _, err := os.Stat(filepath.Join(base, chart.Chart)); os.IsNotExist(err) || chart.Repo != "" { - chartURL, err := chartURL(chart, auth) - if err != nil { - return nil, err - } - - directories = append(directories, directory{ - prefix: checksum(chart), - base: base, - path: chartURL, - key: checksum(chart), - auth: auth, - version: chart.Version, - }) - } - } - return directories, nil -} - -func addDirectory(directories []directory, base, customDir, defaultDir string) ([]directory, error) { - if customDir == "" { - if _, err := os.Stat(filepath.Join(base, defaultDir)); os.IsNotExist(err) { - return directories, nil - } else if err != nil { - return directories, err - } - customDir = defaultDir - } - - return append(directories, directory{ - prefix: defaultDir, - base: base, - path: customDir, - key: defaultDir, - }), nil -} - -type directory struct { - prefix string - base string - path string - key string - version string - auth Auth -} - -func readDirectories(ctx context.Context, compress bool, directories ...directory) (map[string][]fleet.BundleResource, error) { - var ( - sem = semaphore.NewWeighted(4) - result = map[string][]fleet.BundleResource{} - l = sync.Mutex{} - p = progress.NewProgress() - ) - defer p.Close() - - eg, ctx := errgroup.WithContext(ctx) - - for _, dir := range directories { - if err := sem.Acquire(ctx, 1); err != nil { - return nil, err - } - dir := dir - eg.Go(func() error { - defer sem.Release(1) - resources, err := readDirectory(ctx, compress, dir.prefix, dir.base, dir.path, dir.version, dir.auth) - if err != nil { - return err - } - - key := dir.key - if key == "" { - key = dir.path - } - - l.Lock() - result[key] = resources - l.Unlock() - return nil - }) - } - - return result, eg.Wait() -} - -func readDirectory(ctx context.Context, compress bool, prefix, base, name, version string, auth Auth) ([]fleet.BundleResource, error) { - var resources []fleet.BundleResource - - files, err := readContent(ctx, base, name, version, auth) - if err != nil { - return nil, err - } - - for k := range files { - resources = append(resources, fleet.BundleResource{ - Name: k, - }) - } - - for i, resource := range resources { - data := files[resource.Name] - if compress || !utf8.Valid(data) { - content, err := content.Base64GZ(files[resource.Name]) - if err != nil { - return nil, err - } - resources[i].Content = content - resources[i].Encoding = "base64+gz" - } else { - resources[i].Content = string(data) - } - if prefix != "" { - resources[i].Name = filepath.Join(prefix, resources[i].Name) - } - } - - return resources, nil -} - -func readContent(ctx context.Context, base, name, version string, auth Auth) (map[string][]byte, error) { - temp, err := os.MkdirTemp("", "fleet") - if err != nil { - return nil, err - } - defer os.RemoveAll(temp) - - src := name - - // go-getter does not support downloading OCI registry based files yet - // until this is implemented we use Helm to download charts from OCI based registries - // and provide the downloaded file to go-getter locally - if hasOCIURL.MatchString(name) { - src, err = downloadOCIChart(name, version, temp, auth) - if err != nil { - return nil, err - } - } - - temp = filepath.Join(temp, "content") - - base, err = filepath.Abs(base) - if err != nil { - return nil, err - } - - if auth.SSHPrivateKey != nil { - if !strings.ContainsAny(src, "?") { - src += "?" - } else { - src += "&" - } - src += fmt.Sprintf("sshkey=%s", base64.StdEncoding.EncodeToString(auth.SSHPrivateKey)) - } - - // copy getter.Getters before changing - getters := map[string]getter.Getter{} - for k, v := range getter.Getters { - getters[k] = v - } - - httpGetter := newHttpGetter(auth) - getters["http"] = httpGetter - getters["https"] = httpGetter - - c := getter.Client{ - Ctx: ctx, - Src: src, - Dst: temp, - Pwd: base, - Mode: getter.ClientModeDir, - Getters: getters, - // TODO: why doesn't this work anymore - //ProgressListener: progress, - } - - if err := c.Get(); err != nil { - return nil, err - } - - files := map[string][]byte{} - - // dereference link if possible - if dest, err := os.Readlink(temp); err == nil { - temp = dest - } - - err = filepath.Walk(temp, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if info.IsDir() { - if strings.HasPrefix(filepath.Base(path), ".") { - return filepath.SkipDir - } - return nil - } - - name, err := filepath.Rel(temp, path) - if err != nil { - return err - } - - if strings.HasPrefix(filepath.Base(name), ".") { - return nil - } - - content, err := os.ReadFile(path) - if err != nil { - return err - } - - files[name] = content - return nil - }) - if err != nil { - return nil, errors.Wrapf(err, "failed to read %s relative to %s", name, base) - } - - return files, nil -} - -// downloadOciChart uses Helm to download charts from OCI based registries -func downloadOCIChart(name, version, path string, auth Auth) (string, error) { - var registryClient *registry.Client - var requiresLogin bool = auth.Username != "" && auth.Password != "" - - c := downloader.ChartDownloader{ - Verify: downloader.VerifyNever, - Getters: helmgetter.All(&cli.EnvSettings{}), - } - url, err := url.Parse(name) - if err != nil { - return "", err - } - - // Helm does not support direct authentication for private OCI regstries when a chart is downloaded - // so it is necessary to login before via Helm which stores the registry token in a configuration - // file on the system - if requiresLogin { - registryClient, err = registry.NewClient() - if err != nil { - return "", err - } - err = registryClient.Login(url.Hostname(), registry.LoginOptInsecure(false), registry.LoginOptBasicAuth(auth.Username, auth.Password)) - if err != nil { - return "", err - } - } - - saved, _, err := c.DownloadTo(name, version, path) - if err != nil { - return "", err - } - - // Logout to remove the token configuration file from the system again - if requiresLogin { - err = registryClient.Logout(url.Hostname()) - if err != nil { - return "", err - } - } - - return saved, nil -} - -func newHttpGetter(auth Auth) *getter.HttpGetter { - httpGetter := &getter.HttpGetter{ - Client: &http.Client{}, - } - - if auth.Username != "" && auth.Password != "" { - header := http.Header{} - header.Add("Authorization", "Basic "+basicAuth(auth.Username, auth.Password)) - httpGetter.Header = header - } - if auth.CABundle != nil { - pool, err := x509.SystemCertPool() - if err != nil { - pool = x509.NewCertPool() - } - pool.AppendCertsFromPEM(auth.CABundle) - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.TLSClientConfig = &tls.Config{ - RootCAs: pool, - MinVersion: tls.VersionTLS12, - } - httpGetter.Client.Transport = transport - } - return httpGetter -} - -func parseValueFiles(base string, chart *fleet.HelmOptions) (err error) { - if len(chart.ValuesFiles) != 0 { - valuesMap, err := generateValues(base, chart) - if err != nil { - return err - } - - if len(valuesMap.Data) != 0 { - chart.Values = valuesMap - } - } - - return nil -} - -func generateValues(base string, chart *fleet.HelmOptions) (valuesMap *fleet.GenericMap, err error) { - valuesMap = &fleet.GenericMap{} - if chart.Values != nil { - valuesMap = chart.Values - } - for _, value := range chart.ValuesFiles { - valuesByte, err := os.ReadFile(base + "/" + value) - if err != nil { - return nil, err - } - tmpDataOpt := &fleet.GenericMap{} - err = yaml.Unmarshal(valuesByte, tmpDataOpt) - if err != nil { - return nil, err - } - valuesMap = mergeGenericMap(valuesMap, tmpDataOpt) - } - - return valuesMap, nil -} - -func mergeGenericMap(first, second *fleet.GenericMap) *fleet.GenericMap { - result := &fleet.GenericMap{Data: make(map[string]interface{})} - result.Data = data.MergeMaps(first.Data, second.Data) - return result -} - -func basicAuth(username, password string) string { - auth := username + ":" + password - return base64.StdEncoding.EncodeToString([]byte(auth)) -} diff --git a/pkg/bundle/match.go b/pkg/bundlematcher/match.go similarity index 66% rename from pkg/bundle/match.go rename to pkg/bundlematcher/match.go index 5043d27e85..05247ebac3 100644 --- a/pkg/bundle/match.go +++ b/pkg/bundlematcher/match.go @@ -1,29 +1,34 @@ -package bundle +package bundlematcher import ( fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" "github.com/rancher/fleet/pkg/match" ) -type Match struct { - Target *fleet.BundleTarget - Bundle *Bundle +// BundleMatch stores the bundle and the matcher for the bundle +type BundleMatch struct { + bundle *fleet.Bundle + matcher *matcher } -func (a *Bundle) MatchForTarget(name string) *Match { - for i, target := range a.Definition.Spec.Targets { +func New(bundle *fleet.Bundle) (*BundleMatch, error) { + bm := &BundleMatch{ + bundle: bundle, + } + return bm, bm.initMatcher() +} + +func (a *BundleMatch) MatchForTarget(name string) *fleet.BundleTarget { + for i, target := range a.bundle.Spec.Targets { if target.Name != name { continue } - return &Match{ - Target: &a.Definition.Spec.Targets[i], - Bundle: a, - } + return &a.bundle.Spec.Targets[i] } return nil } -func (a *Bundle) Match(clusterName string, clusterGroups map[string]map[string]string, clusterLabels map[string]string) *Match { +func (a *BundleMatch) Match(clusterName string, clusterGroups map[string]map[string]string, clusterLabels map[string]string) *fleet.BundleTarget { for clusterGroup, clusterGroupLabels := range clusterGroups { if m := a.matcher.Match(clusterName, clusterGroup, clusterGroupLabels, clusterLabels); m != nil { return m @@ -36,7 +41,7 @@ func (a *Bundle) Match(clusterName string, clusterGroups map[string]map[string]s } type targetMatch struct { - targetBundle *Match + bundleTarget *fleet.BundleTarget criteria *match.ClusterMatcher } @@ -45,28 +50,25 @@ type matcher struct { restrictions []*match.ClusterMatcher } -func (a *Bundle) initMatcher() error { +func (a *BundleMatch) initMatcher() error { var ( m = &matcher{} ) - for i, target := range a.Definition.Spec.Targets { + for i, target := range a.bundle.Spec.Targets { clusterMatcher, err := match.NewClusterMatcher(target.ClusterName, target.ClusterGroup, target.ClusterGroupSelector, target.ClusterSelector) if err != nil { return err } t := targetMatch{ - targetBundle: &Match{ - Target: &a.Definition.Spec.Targets[i], - Bundle: a, - }, - criteria: clusterMatcher, + bundleTarget: &a.bundle.Spec.Targets[i], + criteria: clusterMatcher, } m.matches = append(m.matches, t) } - for _, target := range a.Definition.Spec.TargetRestrictions { + for _, target := range a.bundle.Spec.TargetRestrictions { clusterMatcher, err := match.NewClusterMatcher(target.ClusterName, target.ClusterGroup, target.ClusterGroupSelector, target.ClusterSelector) if err != nil { return err @@ -92,14 +94,14 @@ func (m *matcher) isRestricted(clusterName, clusterGroup string, clusterGroupLab return true } -func (m *matcher) Match(clusterName, clusterGroup string, clusterGroupLabels, clusterLabels map[string]string) *Match { +func (m *matcher) Match(clusterName, clusterGroup string, clusterGroupLabels, clusterLabels map[string]string) *fleet.BundleTarget { if m.isRestricted(clusterName, clusterGroup, clusterGroupLabels, clusterLabels) { return nil } for _, targetMatch := range m.matches { if targetMatch.criteria.Match(clusterName, clusterGroup, clusterGroupLabels, clusterLabels) { - return targetMatch.targetBundle + return targetMatch.bundleTarget } } diff --git a/pkg/bundlereader/charturl.go b/pkg/bundlereader/charturl.go new file mode 100644 index 0000000000..b01982a7f7 --- /dev/null +++ b/pkg/bundlereader/charturl.go @@ -0,0 +1,102 @@ +package bundlereader + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" + "helm.sh/helm/v3/pkg/repo" + "sigs.k8s.io/yaml" +) + +// chartURL returns the URL to the helm chart from a helm repo server, by +// inspecting the repo's index.yaml +func chartURL(location *fleet.HelmOptions, auth Auth) (string, error) { + // repos are not supported in case of OCI Charts + if hasOCIURL.MatchString(location.Chart) { + return location.Chart, nil + } + + if location.Repo == "" { + return location.Chart, nil + } + + if !strings.HasSuffix(location.Repo, "/") { + location.Repo = location.Repo + "/" + } + + request, err := http.NewRequest("GET", location.Repo+"index.yaml", nil) + if err != nil { + return "", err + } + + if auth.Username != "" && auth.Password != "" { + request.SetBasicAuth(auth.Username, auth.Password) + } + client := &http.Client{} + if auth.CABundle != nil { + pool, err := x509.SystemCertPool() + if err != nil { + pool = x509.NewCertPool() + } + pool.AppendCertsFromPEM(auth.CABundle) + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{ + RootCAs: pool, + MinVersion: tls.VersionTLS12, + } + client.Transport = transport + } + + resp, err := client.Do(request) + if err != nil { + return "", err + } + defer resp.Body.Close() + + bytes, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("failed to read helm repo from %s, error code: %v, response body: %s", location.Repo+"index.yaml", resp.StatusCode, bytes) + } + + repo := &repo.IndexFile{} + if err := yaml.Unmarshal(bytes, repo); err != nil { + return "", err + } + + repo.SortEntries() + + chart, err := repo.Get(location.Chart, location.Version) + if err != nil { + return "", err + } + + if len(chart.URLs) == 0 { + return "", fmt.Errorf("no URLs found for chart %s %s at %s", chart.Name, chart.Version, location.Repo) + } + + chartURL, err := url.Parse(chart.URLs[0]) + if err != nil { + return "", err + } + + if chartURL.IsAbs() { + return chart.URLs[0], nil + } + + repoURL, err := url.Parse(location.Repo) + if err != nil { + return "", err + } + + return repoURL.ResolveReference(chartURL).String(), nil +} diff --git a/pkg/bundlereader/loaddirectory.go b/pkg/bundlereader/loaddirectory.go new file mode 100644 index 0000000000..2ca9d96b3c --- /dev/null +++ b/pkg/bundlereader/loaddirectory.go @@ -0,0 +1,237 @@ +package bundlereader + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "unicode/utf8" + + "github.com/hashicorp/go-getter" + "github.com/pkg/errors" + fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" + "github.com/rancher/fleet/pkg/content" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/downloader" + helmgetter "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/registry" +) + +func loadDirectory(ctx context.Context, compress bool, prefix, base, source, version string, auth Auth) ([]fleet.BundleResource, error) { + var resources []fleet.BundleResource + + files, err := getContent(ctx, base, source, version, auth) + if err != nil { + return nil, err + } + + for k := range files { + resources = append(resources, fleet.BundleResource{ + Name: k, + }) + } + + for i, resource := range resources { + data := files[resource.Name] + if compress || !utf8.Valid(data) { + content, err := content.Base64GZ(files[resource.Name]) + if err != nil { + return nil, err + } + resources[i].Content = content + resources[i].Encoding = "base64+gz" + } else { + resources[i].Content = string(data) + } + if prefix != "" { + resources[i].Name = filepath.Join(prefix, resources[i].Name) + } + } + + return resources, nil +} + +// getContent uses go-getter (and helm for oci) to read the files from directories and servers +func getContent(ctx context.Context, base, source, version string, auth Auth) (map[string][]byte, error) { + temp, err := os.MkdirTemp("", "fleet") + if err != nil { + return nil, err + } + defer os.RemoveAll(temp) + + orgSource := source + + // go-getter does not support downloading OCI registry based files yet + // until this is implemented we use Helm to download charts from OCI based registries + // and provide the downloaded file to go-getter locally + if hasOCIURL.MatchString(source) { + source, err = downloadOCIChart(source, version, temp, auth) + if err != nil { + return nil, err + } + } + + temp = filepath.Join(temp, "content") + + base, err = filepath.Abs(base) + if err != nil { + return nil, err + } + + if auth.SSHPrivateKey != nil { + if !strings.ContainsAny(source, "?") { + source += "?" + } else { + source += "&" + } + source += fmt.Sprintf("sshkey=%s", base64.StdEncoding.EncodeToString(auth.SSHPrivateKey)) + } + + // copy getter.Getters before changing + getters := map[string]getter.Getter{} + for k, v := range getter.Getters { + getters[k] = v + } + + httpGetter := newHttpGetter(auth) + getters["http"] = httpGetter + getters["https"] = httpGetter + + c := getter.Client{ + Ctx: ctx, + Src: source, + Dst: temp, + Pwd: base, + Mode: getter.ClientModeDir, + Getters: getters, + // TODO: why doesn't this work anymore + //ProgressListener: progress, + } + + if err := c.Get(); err != nil { + return nil, err + } + + files := map[string][]byte{} + + // dereference link if possible + if dest, err := os.Readlink(temp); err == nil { + temp = dest + } + + err = filepath.Walk(temp, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + if strings.HasPrefix(filepath.Base(path), ".") { + return filepath.SkipDir + } + return nil + } + + name, err := filepath.Rel(temp, path) + if err != nil { + return err + } + + if strings.HasPrefix(filepath.Base(name), ".") { + return nil + } + + content, err := os.ReadFile(path) + if err != nil { + return err + } + + files[name] = content + return nil + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to read %s relative to %s", orgSource, base) + } + + return files, nil +} + +// downloadOciChart uses Helm to download charts from OCI based registries +func downloadOCIChart(name, version, path string, auth Auth) (string, error) { + var registryClient *registry.Client + var requiresLogin bool = auth.Username != "" && auth.Password != "" + + c := downloader.ChartDownloader{ + Verify: downloader.VerifyNever, + Getters: helmgetter.All(&cli.EnvSettings{}), + } + url, err := url.Parse(name) + if err != nil { + return "", err + } + + // Helm does not support direct authentication for private OCI regstries when a chart is downloaded + // so it is necessary to login before via Helm which stores the registry token in a configuration + // file on the system + if requiresLogin { + registryClient, err = registry.NewClient() + if err != nil { + return "", err + } + err = registryClient.Login(url.Hostname(), registry.LoginOptInsecure(false), registry.LoginOptBasicAuth(auth.Username, auth.Password)) + if err != nil { + return "", err + } + } + + saved, _, err := c.DownloadTo(name, version, path) + if err != nil { + return "", err + } + + // Logout to remove the token configuration file from the system again + if requiresLogin { + err = registryClient.Logout(url.Hostname()) + if err != nil { + return "", err + } + } + + return saved, nil +} + +func newHttpGetter(auth Auth) *getter.HttpGetter { + httpGetter := &getter.HttpGetter{ + Client: &http.Client{}, + } + + if auth.Username != "" && auth.Password != "" { + header := http.Header{} + header.Add("Authorization", "Basic "+basicAuth(auth.Username, auth.Password)) + httpGetter.Header = header + } + if auth.CABundle != nil { + pool, err := x509.SystemCertPool() + if err != nil { + pool = x509.NewCertPool() + } + pool.AppendCertsFromPEM(auth.CABundle) + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{ + RootCAs: pool, + MinVersion: tls.VersionTLS12, + } + httpGetter.Client.Transport = transport + } + return httpGetter +} + +func basicAuth(username, password string) string { + auth := username + ":" + password + return base64.StdEncoding.EncodeToString([]byte(auth)) +} diff --git a/pkg/bundle/read.go b/pkg/bundlereader/read.go similarity index 65% rename from pkg/bundle/read.go rename to pkg/bundlereader/read.go index 8b1334d3a8..31d024ab8a 100644 --- a/pkg/bundle/read.go +++ b/pkg/bundlereader/read.go @@ -1,4 +1,6 @@ -package bundle +// Package bundlereader creates a bundle from a source and adds all the +// referenced resources, as well as image scans. +package bundlereader import ( "bytes" @@ -12,7 +14,7 @@ import ( "strconv" fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" - "github.com/rancher/fleet/pkg/bundleyaml" + "github.com/rancher/fleet/pkg/fleetyaml" name1 "github.com/rancher/wrangler/pkg/name" @@ -31,9 +33,10 @@ type Options struct { Auth Auth } -// Open reads the content, from stdin, or basedir, or a file in basedir. It -// returns a bundle with the given name -func Open(ctx context.Context, name, baseDir, file string, opts *Options) (*Bundle, error) { +// Open reads the fleet.yaml, from stdin, or basedir, or a file in basedir. +// Then it reads/downloads all referenced resources. It returns the populated +// bundle and any existing imagescans. +func Open(ctx context.Context, name, baseDir, file string, opts *Options) (*fleet.Bundle, []*fleet.ImageScan, error) { if baseDir == "" { baseDir = "." } @@ -48,7 +51,7 @@ func Open(ctx context.Context, name, baseDir, file string, opts *Options) (*Bund if file == "" { if file, err := setupIOReader(baseDir); err != nil { - return nil, err + return nil, nil, err } else if file != nil { in = file defer file.Close() @@ -59,7 +62,7 @@ func Open(ctx context.Context, name, baseDir, file string, opts *Options) (*Bund } else { f, err := os.Open(filepath.Join(baseDir, file)) if err != nil { - return nil, err + return nil, nil, err } defer f.Close() in = f @@ -72,14 +75,14 @@ func Open(ctx context.Context, name, baseDir, file string, opts *Options) (*Bund // try the fallback extension. If we receive "IsNotExist" errors for both file extensions, then we return a "nil" file // and a "nil" error. If either return a non-"IsNotExist" error, then we return the error immediately. func setupIOReader(baseDir string) (*os.File, error) { - if file, err := os.Open(bundleyaml.GetFleetYamlPath(baseDir, false)); err != nil && !os.IsNotExist(err) { + if file, err := os.Open(fleetyaml.GetFleetYamlPath(baseDir, false)); err != nil && !os.IsNotExist(err) { return nil, err } else if err == nil { // File must be closed in the parent function. return file, nil } - if file, err := os.Open(bundleyaml.GetFleetYamlPath(baseDir, true)); err != nil && !os.IsNotExist(err) { + if file, err := os.Open(fleetyaml.GetFleetYamlPath(baseDir, true)); err != nil && !os.IsNotExist(err) { return nil, err } else if err == nil { // File must be closed in the parent function. @@ -89,25 +92,25 @@ func setupIOReader(baseDir string) (*os.File, error) { return nil, nil } -func mayCompress(ctx context.Context, name, baseDir string, bundleSpecReader io.Reader, opts *Options) (*Bundle, error) { +func mayCompress(ctx context.Context, name, baseDir string, bundleSpecReader io.Reader, opts *Options) (*fleet.Bundle, []*fleet.ImageScan, error) { if opts == nil { opts = &Options{} } data, err := io.ReadAll(bundleSpecReader) if err != nil { - return nil, err + return nil, nil, err } - bundle, err := read(ctx, name, baseDir, bytes.NewBuffer(data), opts) + bundle, scans, err := read(ctx, name, baseDir, bytes.NewBuffer(data), opts) if err != nil { - return nil, err + return nil, nil, err } - if size, err := size(bundle.Definition); err != nil { - return nil, err + if size, err := size(bundle); err != nil { + return nil, nil, err } else if size < 1000000 { - return bundle, nil + return bundle, scans, nil } newOpts := *opts @@ -123,7 +126,7 @@ func size(bundle *fleet.Bundle) (int, error) { return len(marshalled), nil } -type localSpec struct { +type fleetYAML struct { Name string `json:"name,omitempty"` Labels map[string]string `json:"labels,omitempty"` fleet.BundleSpec @@ -136,7 +139,8 @@ type imageScan struct { fleet.ImageScanSpec } -func read(ctx context.Context, name, baseDir string, bundleSpecReader io.Reader, opts *Options) (*Bundle, error) { +// read reads the fleet.yaml from the bundleSpecReader and loads all resources +func read(ctx context.Context, name, baseDir string, bundleSpecReader io.Reader, opts *Options) (*fleet.Bundle, []*fleet.ImageScan, error) { if opts == nil { opts = &Options{} } @@ -147,21 +151,21 @@ func read(ctx context.Context, name, baseDir string, bundleSpecReader io.Reader, bytes, err := io.ReadAll(bundleSpecReader) if err != nil { - return nil, err + return nil, nil, err } - bundle := &localSpec{} - if err := yaml.Unmarshal(bytes, bundle); err != nil { - return nil, err + fy := &fleetYAML{} + if err := yaml.Unmarshal(bytes, fy); err != nil { + return nil, nil, err } var scans []*fleet.ImageScan - for i, scan := range bundle.ImageScans { + for i, scan := range fy.ImageScans { if scan.Image == "" { continue } if scan.TagName == "" { - return nil, errors.New("the name of scan is required") + return nil, nil, errors.New("the name of scan is required") } scans = append(scans, &fleet.ImageScan{ @@ -172,62 +176,62 @@ func read(ctx context.Context, name, baseDir string, bundleSpecReader io.Reader, }) } - bundle.BundleSpec.Targets = append(bundle.BundleSpec.Targets, bundle.TargetCustomizations...) + fy.BundleSpec.Targets = append(fy.BundleSpec.Targets, fy.TargetCustomizations...) meta, err := readMetadata(bytes) if err != nil { - return nil, err + return nil, nil, err } meta.Name = name - if bundle.Name != "" { - meta.Name = bundle.Name + if fy.Name != "" { + meta.Name = fy.Name } - setTargetNames(&bundle.BundleSpec) + setTargetNames(&fy.BundleSpec) - propagateHelmChartProperties(&bundle.BundleSpec) + propagateHelmChartProperties(&fy.BundleSpec) - resources, err := readResources(ctx, &bundle.BundleSpec, opts.Compress, baseDir, opts.Auth) + resources, err := readResources(ctx, &fy.BundleSpec, opts.Compress, baseDir, opts.Auth) if err != nil { - return nil, err + return nil, nil, err } - bundle.Resources = resources + fy.Resources = resources - def := &fleet.Bundle{ + bundle := &fleet.Bundle{ ObjectMeta: meta.ObjectMeta, - Spec: bundle.BundleSpec, + Spec: fy.BundleSpec, } for k, v := range opts.Labels { - if def.Labels == nil { - def.Labels = make(map[string]string) + if bundle.Labels == nil { + bundle.Labels = make(map[string]string) } - def.Labels[k] = v + bundle.Labels[k] = v } // apply additional labels from spec - for k, v := range bundle.Labels { - if def.Labels == nil { - def.Labels = make(map[string]string) + for k, v := range fy.Labels { + if bundle.Labels == nil { + bundle.Labels = make(map[string]string) } - def.Labels[k] = v + bundle.Labels[k] = v } if opts.ServiceAccount != "" { - def.Spec.ServiceAccount = opts.ServiceAccount + bundle.Spec.ServiceAccount = opts.ServiceAccount } - def.Spec.ForceSyncGeneration = opts.SyncGeneration + bundle.Spec.ForceSyncGeneration = opts.SyncGeneration - def, err = appendTargets(def, opts.TargetsFile) + bundle, err = appendTargets(bundle, opts.TargetsFile) if err != nil { - return nil, err + return nil, nil, err } - if len(def.Spec.Targets) == 0 { - def.Spec.Targets = []fleet.BundleTarget{ + if len(bundle.Spec.Targets) == 0 { + bundle.Spec.Targets = []fleet.BundleTarget{ { Name: "default", ClusterGroup: "default", @@ -236,20 +240,21 @@ func read(ctx context.Context, name, baseDir string, bundleSpecReader io.Reader, } if opts.TargetNamespace != "" { - def.Spec.TargetNamespace = opts.TargetNamespace - for i := range def.Spec.Targets { - def.Spec.Targets[i].TargetNamespace = opts.TargetNamespace + bundle.Spec.TargetNamespace = opts.TargetNamespace + for i := range bundle.Spec.Targets { + bundle.Spec.Targets[i].TargetNamespace = opts.TargetNamespace } } if opts.Paused { - def.Spec.Paused = true + bundle.Spec.Paused = true } - return New(def, scans...) + return bundle, scans, nil } // propagateHelmChartProperties propagates root Helm chart properties to the child targets. +// This is necessary, so we can download the correct chart version for each target. func propagateHelmChartProperties(spec *fleet.BundleSpec) { // Check if there is anything to propagate if spec.Helm == nil { diff --git a/pkg/bundlereader/resources.go b/pkg/bundlereader/resources.go new file mode 100644 index 0000000000..7465c76fec --- /dev/null +++ b/pkg/bundlereader/resources.go @@ -0,0 +1,219 @@ +package bundlereader + +import ( + "context" + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "sync" + + "github.com/rancher/fleet/modules/cli/pkg/progress" + fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" + "golang.org/x/sync/errgroup" + "golang.org/x/sync/semaphore" + + "github.com/rancher/wrangler/pkg/data" + + "sigs.k8s.io/yaml" +) + +var hasOCIURL = regexp.MustCompile(`^oci:\/\/`) + +type Auth struct { + Username string + Password string + CABundle []byte + SSHPrivateKey []byte +} + +// readResources reads and downloads all resources from the bundle +func readResources(ctx context.Context, spec *fleet.BundleSpec, compress bool, base string, auth Auth) ([]fleet.BundleResource, error) { + var directories []directory + + directories, err := addDirectory(directories, base, ".", ".") + if err != nil { + return nil, err + } + + var chartDirs []*fleet.HelmOptions + + if spec.Helm != nil && spec.Helm.Chart != "" { + if err := parseValueFiles(base, spec.Helm); err != nil { + return nil, err + } + chartDirs = append(chartDirs, spec.Helm) + } + + for _, target := range spec.Targets { + if target.Helm != nil { + err := parseValueFiles(base, target.Helm) + if err != nil { + return nil, err + } + if target.Helm.Chart != "" { + chartDirs = append(chartDirs, target.Helm) + } + } + } + + directories, err = addRemoteCharts(directories, base, chartDirs, auth) + if err != nil { + return nil, err + } + + resources, err := loadDirectories(ctx, compress, directories...) + if err != nil { + return nil, err + } + + var result []fleet.BundleResource + for _, resources := range resources { + result = append(result, resources...) + } + + sort.Slice(result, func(i, j int) bool { + return result[i].Name < result[j].Name + }) + + return result, nil +} + +type directory struct { + prefix string + base string + source string + key string + version string + auth Auth +} + +func addDirectory(directories []directory, base, customDir, defaultDir string) ([]directory, error) { + if customDir == "" { + if _, err := os.Stat(filepath.Join(base, defaultDir)); os.IsNotExist(err) { + return directories, nil + } else if err != nil { + return directories, err + } + customDir = defaultDir + } + + return append(directories, directory{ + prefix: defaultDir, + base: base, + source: customDir, + key: defaultDir, + }), nil +} + +func parseValueFiles(base string, chart *fleet.HelmOptions) (err error) { + if len(chart.ValuesFiles) != 0 { + valuesMap, err := generateValues(base, chart) + if err != nil { + return err + } + + if len(valuesMap.Data) != 0 { + chart.Values = valuesMap + } + } + + return nil +} + +func generateValues(base string, chart *fleet.HelmOptions) (valuesMap *fleet.GenericMap, err error) { + valuesMap = &fleet.GenericMap{} + if chart.Values != nil { + valuesMap = chart.Values + } + for _, value := range chart.ValuesFiles { + valuesByte, err := os.ReadFile(base + "/" + value) + if err != nil { + return nil, err + } + tmpDataOpt := &fleet.GenericMap{} + err = yaml.Unmarshal(valuesByte, tmpDataOpt) + if err != nil { + return nil, err + } + valuesMap = mergeGenericMap(valuesMap, tmpDataOpt) + } + + return valuesMap, nil +} + +func mergeGenericMap(first, second *fleet.GenericMap) *fleet.GenericMap { + result := &fleet.GenericMap{Data: make(map[string]interface{})} + result.Data = data.MergeMaps(first.Data, second.Data) + return result +} + +// addRemoteCharts gets the chart url from a helm repo server and returns a `directory` struct. +// For every chart that is not on disk, create a directory struct that contains the charts URL as path. +func addRemoteCharts(directories []directory, base string, charts []*fleet.HelmOptions, auth Auth) ([]directory, error) { + for _, chart := range charts { + if _, err := os.Stat(filepath.Join(base, chart.Chart)); os.IsNotExist(err) || chart.Repo != "" { + chartURL, err := chartURL(chart, auth) + if err != nil { + return nil, err + } + + directories = append(directories, directory{ + prefix: checksum(chart), + base: base, + source: chartURL, + key: checksum(chart), + auth: auth, + version: chart.Version, + }) + } + } + return directories, nil +} + +func checksum(helm *fleet.HelmOptions) string { + if helm == nil { + return "none" + } + return fmt.Sprintf(".chart/%x", sha256.Sum256([]byte(helm.Chart + ":" + helm.Repo + ":" + helm.Version)[:])) +} + +func loadDirectories(ctx context.Context, compress bool, directories ...directory) (map[string][]fleet.BundleResource, error) { + var ( + sem = semaphore.NewWeighted(4) + result = map[string][]fleet.BundleResource{} + l = sync.Mutex{} + p = progress.NewProgress() + ) + defer p.Close() + + eg, ctx := errgroup.WithContext(ctx) + + for _, dir := range directories { + if err := sem.Acquire(ctx, 1); err != nil { + return nil, err + } + dir := dir + eg.Go(func() error { + defer sem.Release(1) + resources, err := loadDirectory(ctx, compress, dir.prefix, dir.base, dir.source, dir.version, dir.auth) + if err != nil { + return err + } + + key := dir.key + if key == "" { + key = dir.source + } + + l.Lock() + result[key] = resources + l.Unlock() + return nil + }) + } + + return result, eg.Wait() +} diff --git a/pkg/bundle/resources_test.go b/pkg/bundlereader/resources_test.go similarity index 99% rename from pkg/bundle/resources_test.go rename to pkg/bundlereader/resources_test.go index ad3dd2749f..961154633c 100644 --- a/pkg/bundle/resources_test.go +++ b/pkg/bundlereader/resources_test.go @@ -1,4 +1,4 @@ -package bundle +package bundlereader import ( "testing" diff --git a/pkg/bundle/style.go b/pkg/bundlereader/style.go similarity index 99% rename from pkg/bundle/style.go rename to pkg/bundlereader/style.go index ca9296dc0c..1c043e36d8 100644 --- a/pkg/bundle/style.go +++ b/pkg/bundlereader/style.go @@ -1,4 +1,4 @@ -package bundle +package bundlereader import ( "path/filepath" diff --git a/pkg/config/config.go b/pkg/config/config.go index 40d2d6331a..b558366f9c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -20,7 +20,10 @@ const ( AgentConfigName = "fleet-agent" AgentBootstrapConfigName = "fleet-agent-bootstrap" Key = "config" - DefaultNamespace = "cattle-fleet-system" + + // DefaultNamespace is the default for the system namespace, which + // contains the manager and agent + DefaultNamespace = "cattle-fleet-system" ) var ( diff --git a/pkg/controllers/bundle/controller.go b/pkg/controllers/bundle/controller.go index db2872e3de..f34e178bfb 100644 --- a/pkg/controllers/bundle/controller.go +++ b/pkg/controllers/bundle/controller.go @@ -173,23 +173,38 @@ func (h *handler) OnBundleChange(bundle *fleet.Bundle, status fleet.BundleStatus logrus.Debugf("OnBundleChange for bundle '%s', checking targets, calculating changes, building objects", bundle.Name) start := time.Now() - targets, err := h.targets.Targets(bundle) + manifest, err := manifest.New(bundle.Spec.Resources) if err != nil { return nil, status, err } - if err := h.calculateChanges(&status, targets); err != nil { + // this does not need to happen after merging the + // BundleDeploymentOptions, since 'fleet apply' already put the right + // resources into bundle.Spec.Resources + if _, err := h.targets.StoreManifest(manifest); err != nil { return nil, status, err } - if err := setResourceKey(&status, bundle, h.isNamespaced, status.ObservedGeneration != bundle.Generation); err != nil { + matchedTargets, err := h.targets.Targets(bundle, manifest) + if err != nil { + return nil, status, err + } + + // NOTE this mutates allTargets and adds new deployments + if err := h.calculateChanges(&status, matchedTargets); err != nil { return nil, status, err } + if status.ObservedGeneration != bundle.Generation { + if err := setResourceKey(&status, bundle, manifest, h.isNamespaced); err != nil { + return nil, status, err + } + } + summary.SetReadyConditions(&status, "Cluster", status.Summary) status.ObservedGeneration = bundle.Generation - objs := toRuntimeObjects(targets, bundle) + objs := bundleDeployments(matchedTargets, bundle) elapsed := time.Since(start) @@ -211,19 +226,12 @@ func (h *handler) isNamespaced(gvk schema.GroupVersionKind) bool { return mapping.Scope.Name() == meta.RESTScopeNameNamespace } -func setResourceKey(status *fleet.BundleStatus, bundle *fleet.Bundle, isNSed func(schema.GroupVersionKind) bool, set bool) error { - if !set { - return nil - } +// setResourceKey runs helm template to set up all resource keys in the BundleStatus passed in as argument. +func setResourceKey(status *fleet.BundleStatus, bundle *fleet.Bundle, manifest *manifest.Manifest, isNSed func(schema.GroupVersionKind) bool) error { bundleMap := map[fleet.ResourceKey]struct{}{} - m, err := manifest.New(&bundle.Spec) - if err != nil { - return err - } - for i := range bundle.Spec.Targets { - opts := options.Calculate(&bundle.Spec, &bundle.Spec.Targets[i]) - objs, err := helmdeployer.Template(bundle.Name, m, opts) + opts := options.Merge(bundle.Spec.BundleDeploymentOptions, bundle.Spec.Targets[i].BundleDeploymentOptions) + objs, err := helmdeployer.Template(bundle.Name, manifest, opts) if err != nil { return err } @@ -249,6 +257,7 @@ func setResourceKey(status *fleet.BundleStatus, bundle *fleet.Bundle, isNSed fun bundleMap[key] = struct{}{} } } + keys := []fleet.ResourceKey{} for k := range bundleMap { keys = append(keys, k) @@ -275,11 +284,13 @@ func setResourceKey(status *fleet.BundleStatus, bundle *fleet.Bundle, isNSed fun return nil } -func toRuntimeObjects(targets []*target.Target, bundle *fleet.Bundle) (result []runtime.Object) { +// bundleDeployments converts the targets to BundleDeployment resources +func bundleDeployments(targets []*target.Target, bundle *fleet.Bundle) (result []runtime.Object) { for _, target := range targets { if target.Deployment == nil { continue } + // NOTE we don't use the existing BundleDeployment, we discard annotations, status, etc dp := &fleet.BundleDeployment{ ObjectMeta: v1.ObjectMeta{ Name: target.Deployment.Name, @@ -295,6 +306,9 @@ func toRuntimeObjects(targets []*target.Target, bundle *fleet.Bundle) (result [] return } +// calculateChanges calculates the changes to the targets, if a target is +// without a deployment, a new BundleDeployment is build +// This func mutates status and allTargets. func (h *handler) calculateChanges(status *fleet.BundleStatus, allTargets []*target.Target) (err error) { // reset status.MaxNew = maxNew @@ -326,12 +340,14 @@ func (h *handler) calculateChanges(status *fleet.BundleStatus, allTargets []*tar newTarget(target, status) } if target.Deployment != nil { + // NOTE merged options from targets.Targets() are set to be staged target.Deployment.Spec.StagedOptions = target.Options target.Deployment.Spec.StagedDeploymentID = target.DeploymentID } } for _, currentTarget := range partition.Targets { + // NOTE this will propagate the merged options to the current deployment updateManifest(currentTarget, status, &partition.Status) } @@ -351,6 +367,8 @@ func (h *handler) calculateChanges(status *fleet.BundleStatus, allTargets []*tar return nil } +// updateManifest will update DeploymentID and Options for the target to the +// staging values, if it's in a deployable state func updateManifest(t *target.Target, status *fleet.BundleStatus, partitionStatus *fleet.PartitionStatus) { if t.Deployment != nil && // Not Paused @@ -363,6 +381,7 @@ func updateManifest(t *target.Target, status *fleet.BundleStatus, partitionStatu (status.Unavailable < status.MaxUnavailable || target.IsUnavailable(t.Deployment)) && // Partition max unavailable not reached (partitionStatus.Unavailable < partitionStatus.MaxUnavailable || target.IsUnavailable(t.Deployment)) { + if !target.IsUnavailable(t.Deployment) { // If this was previously available, now increment unavailable count. "Upgrading" is treated as unavailable. status.Unavailable++ diff --git a/pkg/controllers/cluster/controller.go b/pkg/controllers/cluster/controller.go index e9155e098c..9c23654e24 100644 --- a/pkg/controllers/cluster/controller.go +++ b/pkg/controllers/cluster/controller.go @@ -75,7 +75,7 @@ func Register(ctx context.Context, func (h *handler) ensureNSDeleted(key string, obj *fleet.Cluster) (*fleet.Cluster, error) { if obj == nil { logrus.Debugf("Cluster %s deleted, enqueue cluster namespace deletion", key) - h.namespaces.Enqueue(clusterToNamespace(kv.Split(key, "/"))) + h.namespaces.Enqueue(clusterNamespace(kv.Split(key, "/"))) } return obj, nil } @@ -106,9 +106,10 @@ func (h *handler) findClusters(namespaces corecontrollers.NamespaceCache) relate } } -// clusterToNamespace returns the namespace name for a given cluster name, e.g.: -// cluster-fleet-local-cluster-294db1acfa77-d9ccf852678f -func clusterToNamespace(clusterNamespace, clusterName string) string { +// clusterNamespace returns the cluster namespace name +// for a given cluster name, e.g.: +// "cluster-fleet-local-cluster-294db1acfa77-d9ccf852678f" +func clusterNamespace(clusterNamespace, clusterName string) string { return name.SafeConcatName("cluster", clusterNamespace, clusterName, @@ -136,7 +137,7 @@ func (h *handler) OnClusterChanged(cluster *fleet.Cluster, status fleet.ClusterS } if status.Namespace == "" { - status.Namespace = clusterToNamespace(cluster.Namespace, cluster.Name) + status.Namespace = clusterNamespace(cluster.Namespace, cluster.Name) } bundleDeployments, err := h.bundleDeployment.List(status.Namespace, labels.Everything()) diff --git a/pkg/controllers/cluster/import.go b/pkg/controllers/cluster/import.go index 3c2fc71b83..0cf71b8b4a 100644 --- a/pkg/controllers/cluster/import.go +++ b/pkg/controllers/cluster/import.go @@ -33,7 +33,6 @@ import ( var ( ImportTokenPrefix = "import-token-" - ImportTokenTTL = durations.ClusterImportTokenTTL ) type importHandler struct { @@ -235,7 +234,7 @@ func (i *importHandler) importCluster(cluster *fleet.Cluster, status fleet.Clust }, }, Spec: fleet.ClusterRegistrationTokenSpec{ - TTL: &metav1.Duration{Duration: ImportTokenTTL}, + TTL: &metav1.Duration{Duration: durations.ClusterImportTokenTTL}, }, }) i.clusters.EnqueueAfter(cluster.Namespace, cluster.Name, durations.TokenClusterEnqueueDelay) @@ -313,7 +312,7 @@ func (i *importHandler) importCluster(cluster *fleet.Cluster, status fleet.Clust // Clean up the leftover clusters namespace if it exists. // We want to keep the DefaultNamespace alive, but not the clusters namespace. - err = kc.CoreV1().Namespaces().Delete(i.ctx, fleetns.RegistrationNamespace(config.DefaultNamespace), metav1.DeleteOptions{}) + err = kc.CoreV1().Namespaces().Delete(i.ctx, fleetns.SystemRegistrationNamespace(config.DefaultNamespace), metav1.DeleteOptions{}) if err != nil && !apierrors.IsNotFound(err) { return status, err } diff --git a/pkg/controllers/clusterregistration/controller.go b/pkg/controllers/clusterregistration/controller.go index 72b9df0519..1a1199f575 100644 --- a/pkg/controllers/clusterregistration/controller.go +++ b/pkg/controllers/clusterregistration/controller.go @@ -225,7 +225,7 @@ func (h *handler) OnChange(request *fleet.ClusterRegistration, status fleet.Clus request.Namespace, request.Name, cluster.Namespace, cluster.Name, status.Granted) status.ClusterName = cluster.Name - // e.g. request- in cluster-registration-namespace + // e.g. request- in the cluster namespace return append(objects, &v1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/controllers/controllers.go b/pkg/controllers/controllers.go index 00ee15bada..149325c9c1 100644 --- a/pkg/controllers/controllers.go +++ b/pkg/controllers/controllers.go @@ -77,9 +77,9 @@ func Register(ctx context.Context, systemNamespace string, cfg clientcmd.ClientC return err } - systemRegistrationNamespace := fleetns.RegistrationNamespace(systemNamespace) + systemRegistrationNamespace := fleetns.SystemRegistrationNamespace(systemNamespace) - if err := addData(systemNamespace, systemRegistrationNamespace, appCtx); err != nil { + if err := applyBootstrapResources(systemNamespace, systemRegistrationNamespace, appCtx); err != nil { return err } diff --git a/pkg/controllers/data.go b/pkg/controllers/data.go index 3b5c696b71..38a453047f 100644 --- a/pkg/controllers/data.go +++ b/pkg/controllers/data.go @@ -9,8 +9,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func addData(systemNamespace, systemRegistrationNamespace string, appCtx *appContext) error { - +func applyBootstrapResources(systemNamespace, systemRegistrationNamespace string, appCtx *appContext) error { return appCtx.Apply. WithSetID("fleet-bootstrap-data"). WithDynamicLookup(). diff --git a/pkg/bundleyaml/bundleyaml.go b/pkg/fleetyaml/fleetyaml.go similarity index 85% rename from pkg/bundleyaml/bundleyaml.go rename to pkg/fleetyaml/fleetyaml.go index 08dbdae7e2..15bd4dce24 100644 --- a/pkg/bundleyaml/bundleyaml.go +++ b/pkg/fleetyaml/fleetyaml.go @@ -1,4 +1,6 @@ -package bundleyaml +// Package fleetyaml provides utilities for working with fleet.yaml files, +// which are the central yaml files for bundles. +package fleetyaml import ( "os" diff --git a/pkg/bundleyaml/bundleyaml_test.go b/pkg/fleetyaml/fleetyaml_test.go similarity index 97% rename from pkg/bundleyaml/bundleyaml_test.go rename to pkg/fleetyaml/fleetyaml_test.go index 95ab868b31..19b8516a36 100644 --- a/pkg/bundleyaml/bundleyaml_test.go +++ b/pkg/fleetyaml/fleetyaml_test.go @@ -1,7 +1,7 @@ //go:build !windows // +build !windows -package bundleyaml +package fleetyaml import ( "path/filepath" diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go deleted file mode 100644 index b895460907..0000000000 --- a/pkg/helm/helm.go +++ /dev/null @@ -1,123 +0,0 @@ -package helm - -import ( - "path/filepath" - "strings" - - "helm.sh/helm/v3/pkg/chart" - - fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" - "github.com/rancher/fleet/pkg/bundle" - "github.com/rancher/fleet/pkg/bundleyaml" - "github.com/rancher/fleet/pkg/manifest" - "github.com/rancher/fleet/pkg/rawyaml" - - "github.com/rancher/wrangler/pkg/kv" - - "sigs.k8s.io/yaml" -) - -func Process(name string, m *manifest.Manifest, style bundle.Style) (*manifest.Manifest, error) { - newManifest := toChart(m, style) - if !style.HasChartYAML { - return addChartYAML(name, m, newManifest) - } - return newManifest, nil -} - -func move(m *manifest.Manifest, from, to string) (result []fleet.BundleResource) { - if from == "." { - from = "" - } else if from != "" { - from += "/" - } - for _, resource := range m.Resources { - if strings.HasPrefix(resource.Name, from) { - resource.Name = to + strings.TrimPrefix(resource.Name, from) - result = append(result, resource) - } - } - return result -} - -func manifests(m *manifest.Manifest) (result []fleet.BundleResource) { - var ignorePrefix []string - for _, resource := range m.Resources { - if bundleyaml.IsFleetYamlSuffix(resource.Name) || - strings.HasSuffix(resource.Name, "/Chart.yaml") { - ignorePrefix = append(ignorePrefix, filepath.Dir(resource.Name)+"/") - } - } - -outer: - for _, resource := range m.Resources { - if bundleyaml.IsFleetYaml(resource.Name) { - continue - } - if !strings.HasSuffix(resource.Name, ".yaml") && - !strings.HasSuffix(resource.Name, ".json") && - !strings.HasSuffix(resource.Name, ".yml") { - continue - } - for _, prefix := range ignorePrefix { - if strings.HasPrefix(resource.Name, prefix) { - continue outer - } - } - if strings.HasPrefix(resource.Name, "templates/") { - resource.Name = "chart/" + resource.Name - } else { - resource.Name = rawyaml.YAMLPrefix + resource.Name - } - result = append(result, resource) - } - - return result -} - -func toChart(m *manifest.Manifest, style bundle.Style) *manifest.Manifest { - var ( - resources []fleet.BundleResource - ) - - if style.ChartPath != "" { - resources = move(m, filepath.Dir(style.ChartPath), "chart/") - } else if style.IsRawYAML() { - resources = manifests(m) - } - - return &manifest.Manifest{ - Resources: resources, - Commit: m.Commit, - } -} - -func addChartYAML(name string, m, newManifest *manifest.Manifest) (*manifest.Manifest, error) { - _, hash, err := m.Content() - if err != nil { - return nil, err - } - - if newManifest.Commit != "" && len(newManifest.Commit) > 12 { - hash = "git-" + newManifest.Commit[:12] - } - - _, chartName := kv.RSplit(name, "/") - metadata := chart.Metadata{ - Name: chartName, - Version: "v0.0.0+" + hash, - APIVersion: "v2", - } - - chart, err := yaml.Marshal(metadata) - if err != nil { - return nil, err - } - - newManifest.Resources = append(newManifest.Resources, fleet.BundleResource{ - Name: "chart/Chart.yaml", - Content: string(chart), - }) - - return newManifest, nil -} diff --git a/pkg/helmdeployer/deployer.go b/pkg/helmdeployer/deployer.go index c690e528b1..6147862de3 100644 --- a/pkg/helmdeployer/deployer.go +++ b/pkg/helmdeployer/deployer.go @@ -171,7 +171,7 @@ func (h *Helm) Deploy(bundleID string, manifest *manifest.Manifest, options flee options.Kustomize = &fleet.KustomizeOptions{} } - tar, err := render.ToChart(bundleID, manifest, options) + tar, err := render.HelmChart(bundleID, manifest, options) if err != nil { return nil, err } diff --git a/pkg/helmdeployer/template.go b/pkg/helmdeployer/template.go index d2f897dfff..082977be1f 100644 --- a/pkg/helmdeployer/template.go +++ b/pkg/helmdeployer/template.go @@ -17,6 +17,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// Template runs helm template and returns the resources as a list of objects, without applying them. func Template(bundleID string, manifest *manifest.Manifest, options fleet.BundleDeploymentOptions) ([]runtime.Object, error) { h := &Helm{ globalCfg: action.Configuration{}, diff --git a/pkg/manifest/lookup.go b/pkg/manifest/lookup.go index eb8d136fd2..75881deb3f 100644 --- a/pkg/manifest/lookup.go +++ b/pkg/manifest/lookup.go @@ -31,5 +31,5 @@ func (l *lookup) Get(id string) (*Manifest, error) { return nil, err } - return ReadManifest(bytes, id) + return readManifest(bytes, id) } diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go index 30058515c5..035f2c89c4 100644 --- a/pkg/manifest/manifest.go +++ b/pkg/manifest/manifest.go @@ -20,14 +20,14 @@ type Manifest struct { digest string } -func New(spec *fleet.BundleSpec) (*Manifest, error) { +func New(resources []fleet.BundleResource) (*Manifest, error) { m := &Manifest{ - Resources: spec.Resources, + Resources: resources, } return m, nil } -func ReadManifest(data []byte, digest string) (*Manifest, error) { +func readManifest(data []byte, digest string) (*Manifest, error) { if digest != "" { if _, err := sha256Matches(bytes.NewReader(data), digest); err != nil { return nil, err diff --git a/pkg/namespace/util.go b/pkg/namespace/util.go index 0816669975..8c798c0761 100644 --- a/pkg/namespace/util.go +++ b/pkg/namespace/util.go @@ -1,4 +1,11 @@ -// Package namespace generates the name of the cluster registration namespace. (fleetcontroller) +// Package namespace generates the name of the system registration namespace. (fleetcontroller) +// +// Special namespaces in fleet: +// * system namespace: cattle-fleet-system +// * system registration namespace: cattle-fleet-clusters-system +// * cluster registration namespace or "workspace": fleet-local +// * cluster namespace: cluster-${namespace}-${cluster}-${random} + package namespace import ( @@ -16,10 +23,13 @@ func GVK() schema.GroupVersionKind { } } -func RegistrationNamespace(systemNamespace string) string { - systemRegistrationNamespace := strings.ReplaceAll(systemNamespace, "-system", "-clusters-system") - if systemRegistrationNamespace == systemNamespace { +// SystemRegistrationNamespace generates the name of the system registration +// namespace from the configured system namespace, e.g.: +// cattle-fleet-system -> cattle-fleet-clusters-system +func SystemRegistrationNamespace(systemNamespace string) string { + ns := strings.ReplaceAll(systemNamespace, "-system", "-clusters-system") + if ns == systemNamespace { return systemNamespace + "-clusters-system" } - return systemRegistrationNamespace + return ns } diff --git a/pkg/options/calculate.go b/pkg/options/calculate.go index 8fe3e04084..2a89645d49 100644 --- a/pkg/options/calculate.go +++ b/pkg/options/calculate.go @@ -1,3 +1,4 @@ +// Package options merges the BundleDeploymentOptions package options import ( @@ -10,6 +11,7 @@ import ( "github.com/rancher/wrangler/pkg/data" ) +// DeploymentID hashes the options to a string func DeploymentID(manifest *manifest.Manifest, opts fleet.BundleDeploymentOptions) (string, error) { _, digest, err := manifest.Content() if err != nil { @@ -24,11 +26,8 @@ func DeploymentID(manifest *manifest.Manifest, opts fleet.BundleDeploymentOption return digest + ":" + hex.EncodeToString(h.Sum(nil)), nil } -func Calculate(spec *fleet.BundleSpec, target *fleet.BundleTarget) fleet.BundleDeploymentOptions { - return merge(spec.BundleDeploymentOptions, target.BundleDeploymentOptions) -} - -func merge(base, next fleet.BundleDeploymentOptions) fleet.BundleDeploymentOptions { +// Merge overrides the 'base' options with the 'next' options, if 'next' is present. +func Merge(base, next fleet.BundleDeploymentOptions) fleet.BundleDeploymentOptions { // nolint: gocyclo // business logic result := *base.DeepCopy() if next.DefaultNamespace != "" { result.DefaultNamespace = next.DefaultNamespace diff --git a/pkg/render/helm.go b/pkg/render/helm.go index 14cd5ed0e9..2c05e87c52 100644 --- a/pkg/render/helm.go +++ b/pkg/render/helm.go @@ -2,17 +2,28 @@ package render import ( "io" + "path/filepath" + "strings" + + "helm.sh/helm/v3/pkg/chart" fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" - "github.com/rancher/fleet/pkg/bundle" - "github.com/rancher/fleet/pkg/helm" + "github.com/rancher/fleet/pkg/bundlereader" + "github.com/rancher/fleet/pkg/fleetyaml" "github.com/rancher/fleet/pkg/manifest" "github.com/rancher/fleet/pkg/patch" + "github.com/rancher/fleet/pkg/rawyaml" + + "github.com/rancher/wrangler/pkg/kv" + + "sigs.k8s.io/yaml" ) -func ToChart(name string, m *manifest.Manifest, options fleet.BundleDeploymentOptions) (io.Reader, error) { +// HelmChart applies overlays to "manifest"-style gitrepos and transforms the +// manifest into a helm chart tgz +func HelmChart(name string, m *manifest.Manifest, options fleet.BundleDeploymentOptions) (io.Reader, error) { var ( - style = bundle.DetermineStyle(m, options) + style = bundlereader.DetermineStyle(m, options) err error ) @@ -27,10 +38,118 @@ func ToChart(name string, m *manifest.Manifest, options fleet.BundleDeploymentOp } } - m, err = helm.Process(name, m, style) + m, err = process(name, m, style) if err != nil { return nil, err } return m.ToTarGZ() } + +// process filters the manifests resources and adds a Chart.yaml if missing +func process(name string, m *manifest.Manifest, style bundlereader.Style) (*manifest.Manifest, error) { + newManifest := toChart(m, style) + if !style.HasChartYAML { + return addChartYAML(name, m, newManifest) + } + return newManifest, nil +} + +func move(m *manifest.Manifest, from, to string) (result []fleet.BundleResource) { + if from == "." { + from = "" + } else if from != "" { + from += "/" + } + for _, resource := range m.Resources { + if strings.HasPrefix(resource.Name, from) { + resource.Name = to + strings.TrimPrefix(resource.Name, from) + result = append(result, resource) + } + } + return result +} + +// manifests returns a filtered list of BundleResources +// It also treats the 'templates/' directory as a special case. +func manifests(m *manifest.Manifest) (result []fleet.BundleResource) { + var ignorePrefix []string + for _, resource := range m.Resources { + if fleetyaml.IsFleetYamlSuffix(resource.Name) || + strings.HasSuffix(resource.Name, "/Chart.yaml") { + ignorePrefix = append(ignorePrefix, filepath.Dir(resource.Name)+"/") + } + } + +outer: + for _, resource := range m.Resources { + if fleetyaml.IsFleetYaml(resource.Name) { + continue + } + if !strings.HasSuffix(resource.Name, ".yaml") && + !strings.HasSuffix(resource.Name, ".json") && + !strings.HasSuffix(resource.Name, ".yml") { + continue + } + for _, prefix := range ignorePrefix { + if strings.HasPrefix(resource.Name, prefix) { + continue outer + } + } + if strings.HasPrefix(resource.Name, "templates/") { + resource.Name = "chart/" + resource.Name + } else { + resource.Name = rawyaml.YAMLPrefix + resource.Name + } + result = append(result, resource) + } + + return result +} + +func toChart(m *manifest.Manifest, style bundlereader.Style) *manifest.Manifest { + var ( + resources []fleet.BundleResource + ) + + if style.ChartPath != "" { + resources = move(m, filepath.Dir(style.ChartPath), "chart/") + } else if style.IsRawYAML() { + resources = manifests(m) + } + + return &manifest.Manifest{ + Resources: resources, + Commit: m.Commit, + } +} + +func addChartYAML(name string, m, newManifest *manifest.Manifest) (*manifest.Manifest, error) { + _, hash, err := m.Content() + if err != nil { + return nil, err + } + + if newManifest.Commit != "" && len(newManifest.Commit) > 12 { + hash = "git-" + newManifest.Commit[:12] + } + + _, chartName := kv.RSplit(name, "/") + metadata := chart.Metadata{ + Name: chartName, + Version: "v0.0.0+" + hash, + APIVersion: "v2", + } + + chart, err := yaml.Marshal(metadata) + if err != nil { + return nil, err + } + + newManifest.Resources = append(newManifest.Resources, fleet.BundleResource{ + Name: "chart/Chart.yaml", + Content: string(chart), + }) + + return newManifest, nil +} diff --git a/pkg/target/partition.go b/pkg/target/partition.go index 90852d2ff9..54033d49db 100644 --- a/pkg/target/partition.go +++ b/pkg/target/partition.go @@ -13,6 +13,7 @@ type Partition struct { Targets []*Target } +// Partitions distributes targets into partitions based on the rollout strategy func Partitions(targets []*Target) ([]Partition, error) { rollout := getRollout(targets) if len(rollout.Partitions) == 0 { diff --git a/pkg/target/target.go b/pkg/target/target.go index 494f6d5a1c..f7d4cf8189 100644 --- a/pkg/target/target.go +++ b/pkg/target/target.go @@ -1,4 +1,8 @@ -// Package target provides a functions to match bundles and clusters and to list the bundledeployments for that match. (fleetcontroller) +// Package target provides functionality around building and deploying bundledeployments. (fleetcontroller) +// +// Each "Target" represents a bundle, cluster pair and will be transformed into a bundledeployment. +// The manifest, persisted in the content resource, contains the resources available to +// these bundledeployments. package target import ( @@ -11,7 +15,7 @@ import ( "github.com/sirupsen/logrus" fleet "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1" - "github.com/rancher/fleet/pkg/bundle" + "github.com/rancher/fleet/pkg/bundlematcher" fleetcontrollers "github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io/v1alpha1" "github.com/rancher/fleet/pkg/manifest" "github.com/rancher/fleet/pkg/options" @@ -70,6 +74,12 @@ func (m *Manager) BundleFromDeployment(bd *fleet.BundleDeployment) (string, stri bd.Labels["fleet.cattle.io/bundle-name"] } +// StoreManifest stores the manifest as a content resource and returns the name. +// It copies the resources from the bundle to the content resource. +func (m *Manager) StoreManifest(manifest *manifest.Manifest) (string, error) { + return m.contentStore.Store(manifest) +} + func clusterGroupsToLabelMap(cgs []*fleet.ClusterGroup) map[string]map[string]string { result := map[string]map[string]string{} for _, cg := range cgs { @@ -150,7 +160,7 @@ func (m *Manager) BundlesForCluster(cluster *fleet.Cluster) (bundlesToRefresh, b } for _, app := range bundles { - bundle, err := bundle.New(app) + bm, err := bundlematcher.New(app) if err != nil { logrus.Errorf("ignore bad app %s/%s: %v", app.Namespace, app.Name, err) continue @@ -161,7 +171,7 @@ func (m *Manager) BundlesForCluster(cluster *fleet.Cluster) (bundlesToRefresh, b return nil, nil, err } - match := bundle.Match(cluster.Name, clusterGroupsToLabelMap(cgs), cluster.Labels) + match := bm.Match(cluster.Name, clusterGroupsToLabelMap(cgs), cluster.Labels) if match != nil { bundlesToRefresh = append(bundlesToRefresh, app) } else { @@ -187,16 +197,17 @@ func (m *Manager) GetBundleDeploymentsForBundleInCluster(app *fleet.Bundle, clus return result, nil } -// getNamespacesForBundle returns the namespaces that the bundle should be -// deployed to. Which is the bundles namespace and every namespace from the -// bundle's namespace mappings. -func (m *Manager) getNamespacesForBundle(fleetBundle *fleet.Bundle) ([]string, error) { - mappings, err := m.bundleNamespaceMappingCache.List(fleetBundle.Namespace, labels.Everything()) +// getNamespacesForBundle returns the namespaces that bundledeployments could +// be created in. +// These are the bundle's namespace, e.g. "fleet-local", and every namespace +// matched by a bundle namespace mapping resource. +func (m *Manager) getNamespacesForBundle(bundle *fleet.Bundle) ([]string, error) { + mappings, err := m.bundleNamespaceMappingCache.List(bundle.Namespace, labels.Everything()) if err != nil { return nil, err } - nses := sets.NewString(fleetBundle.Namespace) + nses := sets.NewString(bundle.Namespace) for _, mapping := range mappings { matcher, err := NewBundleMapping(mapping, m.namespaceCache, m.bundleCache) if err != nil { @@ -216,27 +227,24 @@ func (m *Manager) getNamespacesForBundle(fleetBundle *fleet.Bundle) ([]string, e return nses.List(), nil } -// Targets returns all targets for a bundle, so we can create bundledeployments for each -func (m *Manager) Targets(fleetBundle *fleet.Bundle) (result []*Target, _ error) { - bundle, err := bundle.New(fleetBundle) +// Targets returns all targets for a bundle, so we can create bundledeployments for each. +// This is done by checking all namespaces for clusters matching the bundle's +// BundleTarget matchers. +// +// The returned target structs contain merged BundleDeploymentOptions. +// Finally all existing bundledeployments are added to the targets. +func (m *Manager) Targets(bundle *fleet.Bundle, manifest *manifest.Manifest) ([]*Target, error) { + bm, err := bundlematcher.New(bundle) if err != nil { return nil, err } - manifest, err := manifest.New(&bundle.Definition.Spec) - if err != nil { - return nil, err - } - - if _, err := m.contentStore.Store(manifest); err != nil { - return nil, err - } - - namespaces, err := m.getNamespacesForBundle(fleetBundle) + namespaces, err := m.getNamespacesForBundle(bundle) if err != nil { return nil, err } + var targets []*Target for _, namespace := range namespaces { clusters, err := m.clusters.List(namespace, labels.Everything()) if err != nil { @@ -249,12 +257,12 @@ func (m *Manager) Targets(fleetBundle *fleet.Bundle) (result []*Target, _ error) return nil, err } - match := bundle.Match(cluster.Name, clusterGroupsToLabelMap(clusterGroups), cluster.Labels) - if match == nil { + target := bm.Match(cluster.Name, clusterGroupsToLabelMap(clusterGroups), cluster.Labels) + if target == nil { continue } - opts := options.Calculate(&fleetBundle.Spec, match.Target) + opts := options.Merge(bundle.Spec.BundleDeploymentOptions, target.BundleDeploymentOptions) err = addClusterLabels(&opts, cluster.Labels) if err != nil { return nil, err @@ -265,22 +273,22 @@ func (m *Manager) Targets(fleetBundle *fleet.Bundle) (result []*Target, _ error) return nil, err } - result = append(result, &Target{ + targets = append(targets, &Target{ ClusterGroups: clusterGroups, Cluster: cluster, - Target: match.Target, - Bundle: fleetBundle, + Target: target, // contains the unmerged BundleDeploymentOptions + Bundle: bundle, Options: opts, DeploymentID: deploymentID, }) } } - sort.Slice(result, func(i, j int) bool { - return result[i].Cluster.Name < result[j].Cluster.Name + sort.Slice(targets, func(i, j int) bool { + return targets[i].Cluster.Name < targets[j].Cluster.Name }) - return result, m.foldInDeployments(fleetBundle, result) + return targets, m.foldInDeployments(bundle, targets) } func addClusterLabels(opts *fleet.BundleDeploymentOptions, labels map[string]string) (err error) { @@ -328,15 +336,16 @@ func addClusterLabels(opts *fleet.BundleDeploymentOptions, labels map[string]str } -func (m *Manager) foldInDeployments(app *fleet.Bundle, targets []*Target) error { - bundleDeployments, err := m.bundleDeploymentCache.List("", labels.SelectorFromSet(deploymentLabelsForSelector(app))) +// foldInDeployments adds the existing bundledeployments to the targets. +func (m *Manager) foldInDeployments(bundle *fleet.Bundle, targets []*Target) error { + bundleDeployments, err := m.bundleDeploymentCache.List("", labels.SelectorFromSet(deploymentLabelsForSelector(bundle))) if err != nil { return err } byNamespace := map[string]*fleet.BundleDeployment{} - for _, appDep := range bundleDeployments { - byNamespace[appDep.Namespace] = appDep.DeepCopy() + for _, bd := range bundleDeployments { + byNamespace[bd.Namespace] = bd.DeepCopy() } for _, target := range targets { @@ -346,23 +355,23 @@ func (m *Manager) foldInDeployments(app *fleet.Bundle, targets []*Target) error return nil } -func deploymentLabelsForNewBundle(app *fleet.Bundle) map[string]string { - labels := yaml.CleanAnnotationsForExport(app.Labels) - for k, v := range app.Labels { +func deploymentLabelsForNewBundle(bundle *fleet.Bundle) map[string]string { + labels := yaml.CleanAnnotationsForExport(bundle.Labels) + for k, v := range bundle.Labels { if strings.HasPrefix(k, "fleet.cattle.io/") { labels[k] = v } } - for k, v := range deploymentLabelsForSelector(app) { + for k, v := range deploymentLabelsForSelector(bundle) { labels[k] = v } return labels } -func deploymentLabelsForSelector(app *fleet.Bundle) map[string]string { +func deploymentLabelsForSelector(bundle *fleet.Bundle) map[string]string { return map[string]string{ - "fleet.cattle.io/bundle-name": app.Name, - "fleet.cattle.io/bundle-namespace": app.Namespace, + "fleet.cattle.io/bundle-name": bundle.Name, + "fleet.cattle.io/bundle-namespace": bundle.Namespace, } } @@ -381,6 +390,7 @@ func (t *Target) IsPaused() bool { t.Bundle.Spec.Paused } +// AssignNewDeployment builds a new BundleDeployment for the target. func (t *Target) AssignNewDeployment() { labels := map[string]string{} for k, v := range deploymentLabelsForNewBundle(t.Bundle) {