diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 20a8b008..8822f0c9 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -43,13 +43,15 @@ jobs: run: | kubectl apply -f ./config/samples kubectl -n source-system rollout status deploy/source-controller --timeout=1m - kubectl wait gitrepository/podinfo --for=condition=ready --timeout=1m - kubectl wait helmrepository/podinfo --for=condition=ready --timeout=1m + kubectl wait gitrepository/gitrepository-sample --for=condition=ready --timeout=1m + kubectl wait helmrepository/helmrepository-sample --for=condition=ready --timeout=1m + kubectl wait helmchart/helmchart-sample --for=condition=ready --timeout=1m kubectl -n source-system logs deploy/source-controller - name: Debug failure if: failure() run: | kubectl get gitrepositories -oyaml kubectl get helmrepositories -oyaml + kubectl get helmcharts -oyaml kubectl -n source-system get all kubectl -n source-system logs deploy/source-controller diff --git a/PROJECT b/PROJECT index 02c03e29..d910f5b6 100644 --- a/PROJECT +++ b/PROJECT @@ -7,4 +7,7 @@ resources: - group: source kind: HelmRepository version: v1alpha1 +- group: source + kind: HelmChart + version: v1alpha1 version: "2" diff --git a/api/v1alpha1/gitrepository_types.go b/api/v1alpha1/gitrepository_types.go index f4e7cab7..4493f0f4 100644 --- a/api/v1alpha1/gitrepository_types.go +++ b/api/v1alpha1/gitrepository_types.go @@ -76,34 +76,6 @@ type GitRepositoryStatus struct { Artifact *Artifact `json:"artifact,omitempty"` } -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="URL",type=string,JSONPath=`.spec.url` -// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" -// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" -// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" - -// GitRepository is the Schema for the gitrepositories API -type GitRepository struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec GitRepositorySpec `json:"spec,omitempty"` - Status GitRepositoryStatus `json:"status,omitempty"` -} - -// GitRepositoryList contains a list of GitRepository -// +kubebuilder:object:root=true -type GitRepositoryList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []GitRepository `json:"items"` -} - -func init() { - SchemeBuilder.Register(&GitRepository{}, &GitRepositoryList{}) -} - const ( GitOperationSucceedReason string = "GitOperationSucceed" GitOperationFailedReason string = "GitOperationFailed" @@ -153,3 +125,31 @@ func GitRepositoryReadyMessage(repository GitRepository) string { } return "" } + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="URL",type=string,JSONPath=`.spec.url` +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" + +// GitRepository is the Schema for the gitrepositories API +type GitRepository struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GitRepositorySpec `json:"spec,omitempty"` + Status GitRepositoryStatus `json:"status,omitempty"` +} + +// GitRepositoryList contains a list of GitRepository +// +kubebuilder:object:root=true +type GitRepositoryList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GitRepository `json:"items"` +} + +func init() { + SchemeBuilder.Register(&GitRepository{}, &GitRepositoryList{}) +} diff --git a/api/v1alpha1/helmchart_types.go b/api/v1alpha1/helmchart_types.go new file mode 100644 index 00000000..48a2cf20 --- /dev/null +++ b/api/v1alpha1/helmchart_types.go @@ -0,0 +1,153 @@ +/* +Copyright 2020 The Flux CD contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// HelmChartSpec defines the desired state of HelmChart +type HelmChartSpec struct { + // The name of the Helm chart, as made available by the referenced + // Helm repository. + // +required + Name string `json:"name"` + + // The chart version semver expression, defaults to latest when + // omitted. + // +optional + Version string `json:"version,omitempty"` + + // The name of the HelmRepository the chart is available at. + // +required + HelmRepositoryRef corev1.LocalObjectReference `json:"helmRepositoryRef"` + + // The interval at which to check the Helm repository for updates. + // Defaults to the interval of the Helm repository. + // +optional + Interval *metav1.Duration `json:"interval,omitempty"` +} + +// IntervalOrDefault returns the defined interval on the HelmChartSpec +// or the given default. +func (s HelmChartSpec) IntervalOrDefault(interval metav1.Duration) metav1.Duration { + if s.Interval == nil { + return interval + } + return *s.Interval +} + +// HelmChartStatus defines the observed state of HelmChart +type HelmChartStatus struct { + // +optional + Conditions []SourceCondition `json:"conditions,omitempty"` + + // URL is the download link for the last chart pulled. + // +optional + URL string `json:"url,omitempty"` + + // URI for the artifact of the latest successful chart pull. + // +optional + Artifact *Artifact `json:"artifact,omitempty"` +} + +const ( + // ChartPullFailedReason represents the fact that the pull of the + // Helm chart failed. + ChartPullFailedReason string = "ChartPullFailed" + + // ChartPulLSucceededReason represents the fact that the pull of + // the Helm chart succeeded. + ChartPullSucceededReason string = "ChartPullSucceeded" +) + +func HelmChartReady(chart HelmChart, artifact Artifact, url, reason, message string) HelmChart { + chart.Status.Conditions = []SourceCondition{ + { + Type: ReadyCondition, + Status: corev1.ConditionTrue, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + }, + } + chart.Status.URL = url + + if chart.Status.Artifact != nil { + if chart.Status.Artifact.Path != artifact.Path { + chart.Status.Artifact = &artifact + } + } else { + chart.Status.Artifact = &artifact + } + + return chart +} + +func HelmChartNotReady(chart HelmChart, reason, message string) HelmChart { + chart.Status.Conditions = []SourceCondition{ + { + Type: ReadyCondition, + Status: corev1.ConditionFalse, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + }, + } + return chart +} + +func HelmChartReadyMessage(chart HelmChart) string { + for _, condition := range chart.Status.Conditions { + if condition.Type == ReadyCondition { + return condition.Message + } + } + return "" +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Name",type=string,JSONPath=`.spec.name` +// +kubebuilder:printcolumn:name="Version",type=string,JSONPath=`.spec.version` +// +kubebuilder:printcolumn:name="Repository",type=string,JSONPath=`.spec.helmRepositoryRef.name` +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" + +// HelmChart is the Schema for the helmcharts API +type HelmChart struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec HelmChartSpec `json:"spec,omitempty"` + Status HelmChartStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// HelmChartList contains a list of HelmChart +type HelmChartList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []HelmChart `json:"items"` +} + +func init() { + SchemeBuilder.Register(&HelmChart{}, &HelmChartList{}) +} diff --git a/api/v1alpha1/helmrepository_types.go b/api/v1alpha1/helmrepository_types.go index 06e87c8b..665082ed 100644 --- a/api/v1alpha1/helmrepository_types.go +++ b/api/v1alpha1/helmrepository_types.go @@ -47,34 +47,6 @@ type HelmRepositoryStatus struct { Artifact *Artifact `json:"artifact,omitempty"` } -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="URL",type=string,JSONPath=`.spec.url` -// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" -// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" -// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" - -// HelmRepository is the Schema for the helmrepositories API -type HelmRepository struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec HelmRepositorySpec `json:"spec,omitempty"` - Status HelmRepositoryStatus `json:"status,omitempty"` -} - -// HelmRepositoryList contains a list of HelmRepository -// +kubebuilder:object:root=true -type HelmRepositoryList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []HelmRepository `json:"items"` -} - -func init() { - SchemeBuilder.Register(&HelmRepository{}, &HelmRepositoryList{}) -} - const ( // IndexationFailedReason represents the fact that the indexation // of the given Helm repository failed. @@ -129,3 +101,31 @@ func HelmRepositoryReadyMessage(repository HelmRepository) string { } return "" } + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="URL",type=string,JSONPath=`.spec.url` +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" + +// HelmRepository is the Schema for the helmrepositories API +type HelmRepository struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec HelmRepositorySpec `json:"spec,omitempty"` + Status HelmRepositoryStatus `json:"status,omitempty"` +} + +// HelmRepositoryList contains a list of HelmRepository +// +kubebuilder:object:root=true +type HelmRepositoryList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []HelmRepository `json:"items"` +} + +func init() { + SchemeBuilder.Register(&HelmRepository{}, &HelmRepositoryList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f8388374..26bdf49e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ package v1alpha1 import ( "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -168,6 +169,113 @@ func (in *GitRepositoryStatus) DeepCopy() *GitRepositoryStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmChart) DeepCopyInto(out *HelmChart) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChart. +func (in *HelmChart) DeepCopy() *HelmChart { + if in == nil { + return nil + } + out := new(HelmChart) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HelmChart) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmChartList) DeepCopyInto(out *HelmChartList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]HelmChart, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartList. +func (in *HelmChartList) DeepCopy() *HelmChartList { + if in == nil { + return nil + } + out := new(HelmChartList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HelmChartList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmChartSpec) DeepCopyInto(out *HelmChartSpec) { + *out = *in + out.HelmRepositoryRef = in.HelmRepositoryRef + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(metav1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartSpec. +func (in *HelmChartSpec) DeepCopy() *HelmChartSpec { + if in == nil { + return nil + } + out := new(HelmChartSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmChartStatus) DeepCopyInto(out *HelmChartStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]SourceCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Artifact != nil { + in, out := &in.Artifact, &out.Artifact + *out = new(Artifact) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartStatus. +func (in *HelmChartStatus) DeepCopy() *HelmChartStatus { + if in == nil { + return nil + } + out := new(HelmChartStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HelmRepository) DeepCopyInto(out *HelmRepository) { *out = *in diff --git a/config/crd/bases/source.fluxcd.io_helmcharts.yaml b/config/crd/bases/source.fluxcd.io_helmcharts.yaml new file mode 100644 index 00000000..cdc5910e --- /dev/null +++ b/config/crd/bases/source.fluxcd.io_helmcharts.yaml @@ -0,0 +1,153 @@ + +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.5 + creationTimestamp: null + name: helmcharts.source.fluxcd.io +spec: + additionalPrinterColumns: + - JSONPath: .spec.name + name: Name + type: string + - JSONPath: .spec.version + name: Version + type: string + - JSONPath: .spec.helmRepositoryRef.name + name: Repository + type: string + - JSONPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - JSONPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + - JSONPath: .metadata.creationTimestamp + name: Age + type: date + group: source.fluxcd.io + names: + kind: HelmChart + listKind: HelmChartList + plural: helmcharts + singular: helmchart + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: HelmChart is the Schema for the helmcharts API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: HelmChartSpec defines the desired state of HelmChart + properties: + helmRepositoryRef: + description: The name of the HelmRepository the chart is available at. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + interval: + description: The interval at which to check the Helm repository for + updates. Defaults to the interval of the Helm repository. + type: string + name: + description: The name of the Helm chart, as made available by the referenced + Helm repository. + type: string + version: + description: The chart version semver expression, defaults to latest + when omitted. + type: string + required: + - helmRepositoryRef + - name + type: object + status: + description: HelmChartStatus defines the observed state of HelmChart + properties: + artifact: + description: URI for the artifact of the latest successful chart pull. + properties: + lastUpdateTime: + description: LastUpdateTime is the timestamp corresponding to the + last update of this artifact. + format: date-time + type: string + path: + description: Path is the local file path of this artifact. + type: string + revision: + description: Revision is a human readable identifier traceable in + the origin source system. It can be a commit sha, git tag, a helm + index timestamp, a helm chart version, a checksum, etc. + type: string + url: + description: URL is the HTTP address of this artifact. + type: string + required: + - path + - url + type: object + conditions: + items: + description: SourceCondition contains condition information for a + source + properties: + lastTransitionTime: + description: LastTransitionTime is the timestamp corresponding + to the last status change of this condition. + format: date-time + type: string + message: + description: Message is a human readable description of the details + of the last transition, complementing reason. + type: string + reason: + description: Reason is a brief machine readable explanation for + the condition's last transition. + type: string + status: + description: Status of the condition, one of ('True', 'False', + 'Unknown'). + type: string + type: + description: Type of the condition, currently ('Ready'). + type: string + required: + - status + - type + type: object + type: array + url: + description: URL is the download link for the last chart pulled. + type: string + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index a9d6ae22..a724858e 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,5 +3,6 @@ kind: Kustomization resources: - bases/source.fluxcd.io_gitrepositories.yaml - bases/source.fluxcd.io_helmrepositories.yaml +- bases/source.fluxcd.io_helmcharts.yaml # +kubebuilder:scaffold:crdkustomizeresource diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index adf766d2..dbc44c3d 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -6,4 +6,5 @@ resources: - deployment.yaml images: - name: fluxcd/source-controller - newTag: 0.0.1-alpha.1 + newName: fluxcd/source-controller + newTag: latest diff --git a/config/rbac/helmchart_editor_role.yaml b/config/rbac/helmchart_editor_role.yaml new file mode 100644 index 00000000..ef57f522 --- /dev/null +++ b/config/rbac/helmchart_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit helmcharts. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: helmchart-editor-role +rules: +- apiGroups: + - source.fluxcd.io + resources: + - helmcharts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - source.fluxcd.io + resources: + - helmcharts/status + verbs: + - get diff --git a/config/rbac/helmchart_viewer_role.yaml b/config/rbac/helmchart_viewer_role.yaml new file mode 100644 index 00000000..469662d4 --- /dev/null +++ b/config/rbac/helmchart_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view helmcharts. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: helmchart-viewer-role +rules: +- apiGroups: + - source.fluxcd.io + resources: + - helmcharts + verbs: + - get + - list + - watch +- apiGroups: + - source.fluxcd.io + resources: + - helmcharts/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 4e72e864..f3430e25 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -26,6 +26,26 @@ rules: - get - patch - update +- apiGroups: + - source.fluxcd.io + resources: + - helmcharts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - source.fluxcd.io + resources: + - helmcharts/status + verbs: + - get + - patch + - update - apiGroups: - source.fluxcd.io resources: diff --git a/config/samples/source_v1alpha1_gitrepository.yaml b/config/samples/source_v1alpha1_gitrepository.yaml index 673748f9..e7f3ce95 100644 --- a/config/samples/source_v1alpha1_gitrepository.yaml +++ b/config/samples/source_v1alpha1_gitrepository.yaml @@ -1,7 +1,7 @@ apiVersion: source.fluxcd.io/v1alpha1 kind: GitRepository metadata: - name: podinfo + name: gitrepository-sample annotations: source.fluxcd.io/syncAt: "2020-04-06T15:39:52+03:00" spec: diff --git a/config/samples/source_v1alpha1_helmchart.yaml b/config/samples/source_v1alpha1_helmchart.yaml new file mode 100644 index 00000000..44d00e50 --- /dev/null +++ b/config/samples/source_v1alpha1_helmchart.yaml @@ -0,0 +1,9 @@ +apiVersion: source.fluxcd.io/v1alpha1 +kind: HelmChart +metadata: + name: helmchart-sample +spec: + name: podinfo + version: '^2.0.0' + helmRepositoryRef: + name: helmrepository-sample diff --git a/config/samples/source_v1alpha1_helmrepository.yaml b/config/samples/source_v1alpha1_helmrepository.yaml index 05257b5c..1549dae0 100644 --- a/config/samples/source_v1alpha1_helmrepository.yaml +++ b/config/samples/source_v1alpha1_helmrepository.yaml @@ -1,7 +1,7 @@ apiVersion: source.fluxcd.io/v1alpha1 kind: HelmRepository metadata: - name: podinfo + name: helmrepository-sample spec: interval: 1m url: https://stefanprodan.github.io/podinfo diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go new file mode 100644 index 00000000..6b7a8f76 --- /dev/null +++ b/controllers/helmchart_controller.go @@ -0,0 +1,274 @@ +/* +Copyright 2020 The Flux CD contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + "io/ioutil" + "net/url" + "time" + + "github.com/go-logr/logr" + "github.com/pkg/errors" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/yaml" + + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" +) + +// HelmChartReconciler reconciles a HelmChart object +type HelmChartReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + Storage *Storage + Kind string + Getters getter.Providers +} + +// +kubebuilder:rbac:groups=source.fluxcd.io,resources=helmcharts,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=source.fluxcd.io,resources=helmcharts/status,verbs=get;update;patch + +func (r *HelmChartReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + var chart sourcev1.HelmChart + if err := r.Get(ctx, req.NamespacedName, &chart); err != nil { + return ctrl.Result{Requeue: true}, client.IgnoreNotFound(err) + } + + log := r.Log.WithValues(chart.Kind, req.NamespacedName) + + // set initial status + if reset, status := r.shouldResetStatus(chart); reset { + log.Info("Initializing Helm chart") + chart.Status = status + if err := r.Status().Update(ctx, &chart); err != nil { + log.Error(err, "unable to update HelmChart status") + return ctrl.Result{Requeue: true}, err + } + } + + // try to remove old artifacts + r.gc(chart) + + repository, err := r.chartRepository(ctx, chart) + if err != nil { + chart = sourcev1.HelmChartNotReady(*chart.DeepCopy(), sourcev1.ChartPullFailedReason, err.Error()) + if err := r.Status().Update(ctx, &chart); err != nil { + log.Error(err, "unable to update HelmChart status") + return ctrl.Result{Requeue: true}, err + } + return ctrl.Result{Requeue: true}, err + } + + // try to pull chart + pulledChart, err := r.sync(repository, *chart.DeepCopy()) + if err != nil { + log.Info("Helm chart pull failed", "error", err.Error()) + } + + // update status + if err := r.Status().Update(ctx, &pulledChart); err != nil { + log.Error(err, "unable to update HelmChart status") + return ctrl.Result{Requeue: true}, err + } + + log.Info("Helm chart sync succeeded", "msg", sourcev1.HelmChartReadyMessage(pulledChart)) + + // requeue chart + return ctrl.Result{RequeueAfter: repository.Spec.Interval.Duration}, nil +} + +func (r *HelmChartReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&sourcev1.HelmChart{}). + WithEventFilter(RepositoryChangePredicate{}). + WithEventFilter(predicate.Funcs{ + DeleteFunc: func(e event.DeleteEvent) bool { + // delete artifacts + artifact := r.Storage.ArtifactFor(r.Kind, e.Meta, "", "") + if err := r.Storage.RemoveAll(artifact); err != nil { + r.Log.Error(err, "unable to delete artifacts", + r.Kind, fmt.Sprintf("%s/%s", e.Meta.GetNamespace(), e.Meta.GetName())) + } else { + r.Log.Info("Helm chart artifacts deleted", + r.Kind, fmt.Sprintf("%s/%s", e.Meta.GetNamespace(), e.Meta.GetName())) + } + return false + }, + }). + Complete(r) +} + +func (r *HelmChartReconciler) sync(repository sourcev1.HelmRepository, chart sourcev1.HelmChart) (sourcev1.HelmChart, error) { + indexBytes, err := ioutil.ReadFile(repository.Status.Artifact.Path) + if err != nil { + err = fmt.Errorf("failed to read Helm repository index file: %w", err) + return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + } + index := &repo.IndexFile{} + if err := yaml.Unmarshal(indexBytes, index); err != nil { + return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + } + + // find referenced chart in index + cv, err := index.Get(chart.Spec.Name, chart.Spec.Version) + if err != nil { + switch err { + case repo.ErrNoChartName: + err = fmt.Errorf("chart '%s' could not be found in Helm repository '%s'", chart.Spec.Name, repository.Name) + case repo.ErrNoChartVersion: + err = fmt.Errorf("no chart with version '%s' found for '%s'", chart.Spec.Version, chart.Spec.Name) + } + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + + if len(cv.URLs) == 0 { + err = fmt.Errorf("chart '%s' has no downloadable URLs", cv.Name) + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + + // TODO(hidde): according to the Helm source the first item is not + // always the correct one to pick, check for updates once in awhile. + ref := cv.URLs[0] + u, err := url.Parse(ref) + if err != nil { + err = errors.Errorf("invalid chart URL format '%s': %w", ref, err) + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + + // TODO(hidde): auth options from Helm repository probably need to be + // substituted here + c, err := r.Getters.ByScheme(u.Scheme) + if err != nil { + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + + res, err := c.Get(u.String(), getter.WithURL(repository.Spec.URL)) + if err != nil { + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + + chartBytes, err := ioutil.ReadAll(res) + if err != nil { + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + + sum := r.Storage.Checksum(chartBytes) + artifact := r.Storage.ArtifactFor(chart.Kind, chart.GetObjectMeta(), + fmt.Sprintf("%s-%s-%s.tgz", cv.Name, cv.Version, sum), cv.Version) + + // create artifact dir + err = r.Storage.MkdirAll(artifact) + if err != nil { + err = fmt.Errorf("unable to create chart directory: %w", err) + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + + // acquire lock + unlock, err := r.Storage.Lock(artifact) + if err != nil { + err = fmt.Errorf("unable to acquire lock: %w", err) + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + defer unlock() + + // save artifact to storage + err = r.Storage.WriteFile(artifact, chartBytes) + if err != nil { + err = fmt.Errorf("unable to write chart file: %w", err) + return sourcev1.HelmChartNotReady(chart, sourcev1.ChartPullFailedReason, err.Error()), err + } + + // update index symlink + chartUrl, err := r.Storage.Symlink(artifact, fmt.Sprintf("%s-latest.tgz", cv.Name)) + if err != nil { + err = fmt.Errorf("storage error: %w", err) + return sourcev1.HelmChartNotReady(chart, sourcev1.StorageOperationFailedReason, err.Error()), err + } + + message := fmt.Sprintf("Helm chart is available at: %s", artifact.Path) + return sourcev1.HelmChartReady(chart, artifact, chartUrl, sourcev1.ChartPullSucceededReason, message), nil +} + +func (r *HelmChartReconciler) chartRepository(ctx context.Context, chart sourcev1.HelmChart) (sourcev1.HelmRepository, error) { + if chart.Spec.HelmRepositoryRef.Name == "" { + return sourcev1.HelmRepository{}, fmt.Errorf("no HelmRepository reference given") + } + + name := types.NamespacedName{ + Namespace: chart.GetNamespace(), + Name: chart.Spec.HelmRepositoryRef.Name, + } + + var repository sourcev1.HelmRepository + err := r.Client.Get(ctx, name, &repository) + if err != nil { + err = fmt.Errorf("failed to get HelmRepository '%s': %w", name, err) + } + + if repository.Status.Artifact == nil { + err = fmt.Errorf("no repository index artifect found in HelmRepository '%s'", repository.Name) + } + + return repository, err +} + +func (r *HelmChartReconciler) shouldResetStatus(chart sourcev1.HelmChart) (bool, sourcev1.HelmChartStatus) { + resetStatus := false + if chart.Status.Artifact != nil { + if !r.Storage.ArtifactExist(*chart.Status.Artifact) { + resetStatus = true + } + } + + // set initial status + if len(chart.Status.Conditions) == 0 || resetStatus { + resetStatus = true + } + + return resetStatus, sourcev1.HelmChartStatus{ + Conditions: []sourcev1.SourceCondition{ + { + Type: sourcev1.ReadyCondition, + Status: corev1.ConditionUnknown, + Reason: sourcev1.InitializingReason, + LastTransitionTime: metav1.Now(), + }, + }, + } +} + +func (r *HelmChartReconciler) gc(chart sourcev1.HelmChart) { + if chart.Status.Artifact != nil { + if err := r.Storage.RemoveAllButCurrent(*chart.Status.Artifact); err != nil { + r.Log.Info("Artifacts GC failed", "error", err) + } + } +} diff --git a/controllers/helmrepository_controller.go b/controllers/helmrepository_controller.go index 5b52d81b..bf24f490 100644 --- a/controllers/helmrepository_controller.go +++ b/controllers/helmrepository_controller.go @@ -25,7 +25,6 @@ import ( "time" "github.com/go-logr/logr" - "gopkg.in/yaml.v2" "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/repo" corev1 "k8s.io/api/core/v1" @@ -35,6 +34,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/yaml" sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" ) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 3bce0d52..3a1e41c4 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -68,6 +68,9 @@ var _ = BeforeSuite(func(done Done) { err = sourcev1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = sourcev1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) diff --git a/go.mod b/go.mod index 5ee64e3c..cd6e9bc8 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,11 @@ require ( github.com/go-logr/logr v0.1.0 github.com/onsi/ginkgo v1.11.0 github.com/onsi/gomega v1.8.1 - gopkg.in/yaml.v2 v2.2.4 + github.com/pkg/errors v0.9.1 helm.sh/helm/v3 v3.1.2 k8s.io/api v0.17.2 k8s.io/apimachinery v0.17.2 k8s.io/client-go v0.17.2 sigs.k8s.io/controller-runtime v0.5.0 + sigs.k8s.io/yaml v1.1.0 ) diff --git a/go.sum b/go.sum index c99bfccf..19715f6b 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= @@ -116,6 +117,7 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= @@ -275,6 +277,7 @@ github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= @@ -295,6 +298,7 @@ github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -410,6 +414,7 @@ github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -629,8 +634,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -helm.sh/helm v2.16.5+incompatible h1:cWIBFS2bwUEgWSIggw4gvwdWFH0BLE54X7/3WszxjWc= helm.sh/helm/v3 v3.1.2 h1:VpNzaNv2DX4aRnOCcV7v5Of+XT2SZrJ8iOQ25AGKOos= helm.sh/helm/v3 v3.1.2/go.mod h1:WYsFJuMASa/4XUqLyv54s0U/f3mlAaRErGmyy4z921g= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index 44de5f36..d5285598 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,12 @@ import ( var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") + getters = getter.Providers{ + getter.Provider{ + Schemes: []string{"http", "https"}, + New: getter.NewHTTPGetter, + }, + } ) func init() { @@ -98,16 +104,22 @@ func main() { Scheme: mgr.GetScheme(), Kind: "helmrepository", Storage: storage, - Getters: getter.Providers{ - getter.Provider{ - Schemes: []string{"http", "https"}, - New: getter.NewHTTPGetter, - }, - }, + Getters: getters, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "HelmRepository") os.Exit(1) } + if err = (&controllers.HelmChartReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("HelmChart"), + Scheme: mgr.GetScheme(), + Kind: "helmchart", + Storage: storage, + Getters: getters, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "HelmChart") + os.Exit(1) + } // +kubebuilder:scaffold:builder setupLog.Info("starting manager")