From 8b033300098ef0affc41873b286afd5e3d7d0a93 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Fri, 31 May 2024 13:32:21 +0300 Subject: [PATCH] Add persistent storage for Flux artifacts Signed-off-by: Stefan Prodan --- README.md | 3 + api/v1/fluxinstance_types.go | 16 ++ api/v1/zz_generated.deepcopy.go | 20 +++ .../fluxcd.controlplane.io_fluxinstances.yaml | 15 ++ config/samples/fluxcd_v1_fluxinstance.yaml | 3 + internal/builder/build.go | 6 + internal/builder/build_test.go | 62 +++++++- internal/builder/images.go | 8 - internal/builder/options.go | 15 ++ internal/builder/templates.go | 34 ++++- .../v2.3.0-golden/storage.kustomization.yaml | 142 ++++++++++++++++++ .../controller/fluxinstance_controller.go | 7 + 12 files changed, 318 insertions(+), 13 deletions(-) create mode 100644 internal/builder/testdata/v2.3.0-golden/storage.kustomization.yaml diff --git a/README.md b/README.md index b6d935c..6384396 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,9 @@ spec: multitenant: true networkPolicy: true domain: "cluster.local" + storage: + class: "standard" + size: "10Gi" ``` Every hour, the operator will check for updates in the ControlPlane diff --git a/api/v1/fluxinstance_types.go b/api/v1/fluxinstance_types.go index 2a031a0..2c1f228 100644 --- a/api/v1/fluxinstance_types.go +++ b/api/v1/fluxinstance_types.go @@ -44,6 +44,11 @@ type FluxInstanceSpec struct { // +optional Cluster *Cluster `json:"cluster,omitempty"` + // Storage holds the specification of the source-controller + // persistent volume claim. + // +optional + Storage *Storage `json:"storage,omitempty"` + // Kustomize holds a set of patches that can be applied to the // Flux installation, to customize the way Flux operates. // +optional @@ -122,6 +127,17 @@ type Cluster struct { Type string `json:"type,omitempty"` } +// Storage is the specification for the persistent volume claim. +type Storage struct { + // Class is the storage class to use for the PVC. + // +required + Class string `json:"class"` + + // Size is the size of the PVC. + // +required + Size string `json:"size"` +} + // Kustomize holds a set of patches that can be applied to the // Flux installation, to customize the way Flux operates. type Kustomize struct { diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 0d87c90..488917e 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -131,6 +131,11 @@ func (in *FluxInstanceSpec) DeepCopyInto(out *FluxInstanceSpec) { *out = new(Cluster) **out = **in } + if in.Storage != nil { + in, out := &in.Storage, &out.Storage + *out = new(Storage) + **out = **in + } if in.Kustomize != nil { in, out := &in.Kustomize, &out.Kustomize *out = new(Kustomize) @@ -237,3 +242,18 @@ func (in *ResourceRef) DeepCopy() *ResourceRef { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Storage) DeepCopyInto(out *Storage) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Storage. +func (in *Storage) DeepCopy() *Storage { + if in == nil { + return nil + } + out := new(Storage) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/fluxcd.controlplane.io_fluxinstances.yaml b/config/crd/bases/fluxcd.controlplane.io_fluxinstances.yaml index 2ce27dc..89f7b05 100644 --- a/config/crd/bases/fluxcd.controlplane.io_fluxinstances.yaml +++ b/config/crd/bases/fluxcd.controlplane.io_fluxinstances.yaml @@ -185,6 +185,21 @@ spec: type: object type: array type: object + storage: + description: |- + Storage holds the specification of the source-controller + persistent volume claim. + properties: + class: + description: Class is the storage class to use for the PVC. + type: string + size: + description: Size is the size of the PVC. + type: string + required: + - class + - size + type: object wait: default: true description: |- diff --git a/config/samples/fluxcd_v1_fluxinstance.yaml b/config/samples/fluxcd_v1_fluxinstance.yaml index bb9836a..17cfb7e 100644 --- a/config/samples/fluxcd_v1_fluxinstance.yaml +++ b/config/samples/fluxcd_v1_fluxinstance.yaml @@ -18,6 +18,9 @@ spec: multitenant: false networkPolicy: true domain: "cluster.local" + storage: + class: "standard" + size: "1Gi" kustomize: patches: - target: diff --git a/internal/builder/build.go b/internal/builder/build.go index b3b1f47..2b48afd 100644 --- a/internal/builder/build.go +++ b/internal/builder/build.go @@ -74,6 +74,12 @@ func generate(base string, options Options) error { return fmt.Errorf("generate node selector failed: %w", err) } + if options.ArtifactStorage != nil { + if err := execTemplate(options, pvcTmpl, path.Join(base, "pvc.yaml")); err != nil { + return fmt.Errorf("generate pvc failed: %w", err) + } + } + if err := execTemplate(options, kustomizationTmpl, path.Join(base, "kustomization.yaml")); err != nil { return fmt.Errorf("generate kustomization failed: %w", err) } diff --git a/internal/builder/build_test.go b/internal/builder/build_test.go index 3b05513..09c9375 100644 --- a/internal/builder/build_test.go +++ b/internal/builder/build_test.go @@ -15,6 +15,7 @@ import ( "sigs.k8s.io/yaml" ) +//nolint:goconst func TestBuild_Defaults(t *testing.T) { g := NewWithT(t) const version = "v2.3.0" @@ -49,6 +50,7 @@ func TestBuild_Defaults(t *testing.T) { g.Expect(string(genK)).To(Equal(string(goldenK))) } +//nolint:goconst func TestBuild_Patches(t *testing.T) { g := NewWithT(t) const version = "v2.3.0" @@ -113,6 +115,7 @@ func TestBuild_Patches(t *testing.T) { g.Expect(found).To(BeTrue()) } +//nolint:goconst func TestBuild_Profiles(t *testing.T) { g := NewWithT(t) const version = "v2.3.0" @@ -136,10 +139,10 @@ func TestBuild_Profiles(t *testing.T) { g.Expect(result.Objects).NotTo(BeEmpty()) g.Expect(result.Revision).To(HavePrefix(version + "@sha256:")) - //if os.Getenv("GEN_GOLDEN") == "true" { - err = cp.Copy(filepath.Join(dstDir, "kustomization.yaml"), goldenFile) - g.Expect(err).NotTo(HaveOccurred()) - //} + if os.Getenv("GEN_GOLDEN") == "true" { + err = cp.Copy(filepath.Join(dstDir, "kustomization.yaml"), goldenFile) + g.Expect(err).NotTo(HaveOccurred()) + } genK, err := os.ReadFile(filepath.Join(dstDir, "kustomization.yaml")) g.Expect(err).NotTo(HaveOccurred()) @@ -161,6 +164,57 @@ func TestBuild_Profiles(t *testing.T) { g.Expect(found).To(BeTrue()) } +//nolint:goconst +func TestBuild_ArtifactStorage(t *testing.T) { + g := NewWithT(t) + const version = "v2.3.0" + options := MakeDefaultOptions() + options.Version = version + + srcDir := filepath.Join("testdata", version) + goldenFile := filepath.Join("testdata", version+"-golden", "storage.kustomization.yaml") + + dstDir, err := testTempDir(t) + g.Expect(err).NotTo(HaveOccurred()) + + ci, err := ExtractComponentImages(srcDir, options) + g.Expect(err).NotTo(HaveOccurred()) + options.ComponentImages = ci + + options.ArtifactStorage = &ArtifactStorage{ + Class: "standard", + Size: "10Gi", + } + + result, err := Build(srcDir, dstDir, options) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(result.Objects).NotTo(BeEmpty()) + g.Expect(result.Revision).To(HavePrefix(version + "@sha256:")) + + if os.Getenv("GEN_GOLDEN") == "true" { + err = cp.Copy(filepath.Join(dstDir, "kustomization.yaml"), goldenFile) + g.Expect(err).NotTo(HaveOccurred()) + } + + genK, err := os.ReadFile(filepath.Join(dstDir, "kustomization.yaml")) + g.Expect(err).NotTo(HaveOccurred()) + + goldenK, err := os.ReadFile(goldenFile) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(string(genK)).To(Equal(string(goldenK))) + + found := false + for _, obj := range result.Objects { + if obj.GetKind() == "PersistentVolumeClaim" { + found = true + g.Expect(obj.GetName()).To(Equal("source-controller")) + } + } + g.Expect(found).To(BeTrue()) +} + +//nolint:goconst func TestBuild_InvalidPatches(t *testing.T) { g := NewWithT(t) const version = "v2.3.0" diff --git a/internal/builder/images.go b/internal/builder/images.go index 0d5e54d..77d18e0 100644 --- a/internal/builder/images.go +++ b/internal/builder/images.go @@ -21,14 +21,6 @@ import ( "sigs.k8s.io/yaml" ) -// ComponentImage represents a container image used by a component. -type ComponentImage struct { - Name string - Repository string - Tag string - Digest string -} - // ExtractComponentImages reads the source directory and extracts the container images // from the components manifests. func ExtractComponentImages(srcDir string, opts Options) ([]ComponentImage, error) { diff --git a/internal/builder/options.go b/internal/builder/options.go index ac361ef..4449121 100644 --- a/internal/builder/options.go +++ b/internal/builder/options.go @@ -19,6 +19,7 @@ type Options struct { ClusterDomain string TolerationKeys []string Patches string + ArtifactStorage *ArtifactStorage } // MakeDefaultOptions returns the default builder configuration. @@ -44,3 +45,17 @@ func MakeDefaultOptions() Options { ClusterDomain: "cluster.local", } } + +// ComponentImage represents a container image used by a component. +type ComponentImage struct { + Name string + Repository string + Tag string + Digest string +} + +// ArtifactStorage represents the source-controller PVC. +type ArtifactStorage struct { + Class string + Size string +} diff --git a/internal/builder/templates.go b/internal/builder/templates.go index d2bad66..f740046 100644 --- a/internal/builder/templates.go +++ b/internal/builder/templates.go @@ -17,9 +17,11 @@ var kustomizationTmpl = `--- {{- $registry := .Registry }} {{- $logLevel := .LogLevel }} {{- $clusterDomain := .ClusterDomain }} +{{- $artifactStorage := .ArtifactStorage }} +{{- $namespace := .Namespace }} apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: {{.Namespace}} +namespace: {{$namespace}} transformers: - labels.yaml resources: @@ -31,6 +33,9 @@ resources: {{- range .Components }} - {{.}}.yaml {{- end }} +{{- if $artifactStorage }} + - pvc.yaml +{{- end }} {{- if $registry }} images: {{- range .ComponentImages }} @@ -79,6 +84,19 @@ patches: - op: replace path: /spec/template/spec/containers/0/args/6 value: --storage-adv-addr=source-controller.$(RUNTIME_NAMESPACE).svc.{{$clusterDomain}}. +{{- if $artifactStorage }} + - op: add + path: '/spec/template/spec/volumes/-' + value: + name: persistent-data + persistentVolumeClaim: + claimName: source-controller + - op: replace + path: '/spec/template/spec/containers/0/volumeMounts/0' + value: + name: persistent-data + mountPath: /data +{{- end }} {{- else }} - target: group: apps @@ -159,6 +177,20 @@ metadata: fluxcd.controlplane.io/prune: disabled ` +var pvcTmpl = `--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: source-controller +spec: + accessModes: + - ReadWriteOnce + storageClassName: {{.ArtifactStorage.Class}} + resources: + requests: + storage: {{.ArtifactStorage.Size}} +` + func execTemplate(obj interface{}, tmpl, filename string) (err error) { t, err := template.New("tmpl").Parse(tmpl) if err != nil { diff --git a/internal/builder/testdata/v2.3.0-golden/storage.kustomization.yaml b/internal/builder/testdata/v2.3.0-golden/storage.kustomization.yaml new file mode 100644 index 0000000..e7d9470 --- /dev/null +++ b/internal/builder/testdata/v2.3.0-golden/storage.kustomization.yaml @@ -0,0 +1,142 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: flux-system +transformers: + - labels.yaml +resources: + - namespace.yaml + - policies.yaml + - roles + - source-controller.yaml + - kustomize-controller.yaml + - helm-controller.yaml + - notification-controller.yaml + - image-reflector-controller.yaml + - image-automation-controller.yaml + - pvc.yaml +images: + - name: fluxcd/source-controller + newName: ghcr.io/fluxcd/source-controller + newTag: v1.3.0 + - name: fluxcd/kustomize-controller + newName: ghcr.io/fluxcd/kustomize-controller + newTag: v1.3.0 + - name: fluxcd/helm-controller + newName: ghcr.io/fluxcd/helm-controller + newTag: v1.0.1 + - name: fluxcd/notification-controller + newName: ghcr.io/fluxcd/notification-controller + newTag: v1.3.0 + - name: fluxcd/image-reflector-controller + newName: ghcr.io/fluxcd/image-reflector-controller + newTag: v0.32.0 + - name: fluxcd/image-automation-controller + newName: ghcr.io/fluxcd/image-automation-controller + newTag: v0.38.0 +patches: +- path: node-selector.yaml + target: + kind: Deployment +- target: + group: apps + version: v1 + kind: Deployment + name: source-controller + patch: |- + - op: replace + path: /spec/template/spec/containers/0/args/0 + value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ + - op: replace + path: /spec/template/spec/containers/0/args/1 + value: --watch-all-namespaces=true + - op: replace + path: /spec/template/spec/containers/0/args/2 + value: --log-level=info + - op: replace + path: /spec/template/spec/containers/0/args/6 + value: --storage-adv-addr=source-controller.$(RUNTIME_NAMESPACE).svc.cluster.local. + - op: add + path: '/spec/template/spec/volumes/-' + value: + name: persistent-data + persistentVolumeClaim: + claimName: source-controller + - op: replace + path: '/spec/template/spec/containers/0/volumeMounts/0' + value: + name: persistent-data + mountPath: /data +- target: + group: apps + version: v1 + kind: Deployment + name: kustomize-controller + patch: |- + - op: replace + path: /spec/template/spec/containers/0/args/0 + value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ + - op: replace + path: /spec/template/spec/containers/0/args/1 + value: --watch-all-namespaces=true + - op: replace + path: /spec/template/spec/containers/0/args/2 + value: --log-level=info +- target: + group: apps + version: v1 + kind: Deployment + name: helm-controller + patch: |- + - op: replace + path: /spec/template/spec/containers/0/args/0 + value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ + - op: replace + path: /spec/template/spec/containers/0/args/1 + value: --watch-all-namespaces=true + - op: replace + path: /spec/template/spec/containers/0/args/2 + value: --log-level=info +- target: + group: apps + version: v1 + kind: Deployment + name: notification-controller + patch: |- + - op: replace + path: /spec/template/spec/containers/0/args/0 + value: --watch-all-namespaces=true + - op: replace + path: /spec/template/spec/containers/0/args/1 + value: --log-level=info +- target: + group: apps + version: v1 + kind: Deployment + name: image-reflector-controller + patch: |- + - op: replace + path: /spec/template/spec/containers/0/args/0 + value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ + - op: replace + path: /spec/template/spec/containers/0/args/1 + value: --watch-all-namespaces=true + - op: replace + path: /spec/template/spec/containers/0/args/2 + value: --log-level=info +- target: + group: apps + version: v1 + kind: Deployment + name: image-automation-controller + patch: |- + - op: replace + path: /spec/template/spec/containers/0/args/0 + value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ + - op: replace + path: /spec/template/spec/containers/0/args/1 + value: --watch-all-namespaces=true + - op: replace + path: /spec/template/spec/containers/0/args/2 + value: --log-level=info + diff --git a/internal/controller/fluxinstance_controller.go b/internal/controller/fluxinstance_controller.go index 614ac10..3d89bac 100644 --- a/internal/controller/fluxinstance_controller.go +++ b/internal/controller/fluxinstance_controller.go @@ -207,6 +207,13 @@ func (r *FluxInstanceReconciler) build(ctx context.Context, options.Patches += builder.ProfileMultitenant } + if obj.Spec.Storage != nil { + options.ArtifactStorage = &builder.ArtifactStorage{ + Class: obj.Spec.Storage.Class, + Size: obj.Spec.Storage.Size, + } + } + if obj.Spec.Kustomize != nil && len(obj.Spec.Kustomize.Patches) > 0 { patchesData, err := yaml.Marshal(obj.Spec.Kustomize.Patches) if err != nil {