Introduce HelmChart API and controller

- Add the HelmChart types and controller
- Semver expressions are found by utilizing Helm repository index
  helpers. As Helm makes use of `masterminds/semver`, the support
  for i.e. ranges less mature than the `GitRepository` implementation.
- Recorded semver is as defined in the metadata of the chart. The
  used name for the artifact does however include the checksum of the
  chart archive, as chart maintainers may not always properly apply
  semver.
- Switches to `sigs.k8s.io/yaml` for YAML operations as this among
  other things is able to properly unmarshal embedded structures.
- Directly requeues on transient errors instead of using the defined
  interval as a back-off strategy is applied on repeated failures.
This commit is contained in:
Hidde Beydals 2020-04-11 22:52:58 +02:00
parent f9a35a6613
commit d378bd1852
21 changed files with 859 additions and 70 deletions

View File

@ -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

View File

@ -7,4 +7,7 @@ resources:
- group: source
kind: HelmRepository
version: v1alpha1
- group: source
kind: HelmChart
version: v1alpha1
version: "2"

View File

@ -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{})
}

View File

@ -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{})
}

View File

@ -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{})
}

View File

@ -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

View File

@ -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: []

View File

@ -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

View File

@ -6,4 +6,5 @@ resources:
- deployment.yaml
images:
- name: fluxcd/source-controller
newTag: 0.0.1-alpha.1
newName: fluxcd/source-controller
newTag: latest

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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"
)

View File

@ -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})

3
go.mod
View File

@ -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
)

7
go.sum
View File

@ -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=

24
main.go
View File

@ -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")