/ *
Copyright 2020 The Flux authors
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 main
import (
"context"
"fmt"
"io/ioutil"
"net/url"
"os"
"github.com/fluxcd/pkg/apis/meta"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/flux2/internal/utils"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
)
var createSourceHelmCmd = & cobra . Command {
Use : "helm [name]" ,
Short : "Create or update a HelmRepository source" ,
Long : `
The create source helm command generates a HelmRepository resource and waits for it to fetch the index .
For private Helm repositories , the basic authentication credentials are stored in a Kubernetes secret . ` ,
Example : ` # Create a source from a public Helm repository
flux create source helm podinfo \
-- url = https : //stefanprodan.github.io/podinfo \
-- interval = 10 m
# Create a source from a Helm repository using basic authentication
flux create source helm podinfo \
-- url = https : //stefanprodan.github.io/podinfo \
-- username = username \
-- password = password
# Create a source from a Helm repository using TLS authentication
flux create source helm podinfo \
-- url = https : //stefanprodan.github.io/podinfo \
-- cert - file = . / cert . crt \
-- key - file = . / key . crt \
-- ca - file = . / ca . crt
` ,
RunE : createSourceHelmCmdRun ,
}
type sourceHelmFlags struct {
url string
username string
password string
certFile string
keyFile string
caFile string
secretRef string
}
var sourceHelmArgs sourceHelmFlags
func init ( ) {
createSourceHelmCmd . Flags ( ) . StringVar ( & sourceHelmArgs . url , "url" , "" , "Helm repository address" )
createSourceHelmCmd . Flags ( ) . StringVarP ( & sourceHelmArgs . username , "username" , "u" , "" , "basic authentication username" )
createSourceHelmCmd . Flags ( ) . StringVarP ( & sourceHelmArgs . password , "password" , "p" , "" , "basic authentication password" )
createSourceHelmCmd . Flags ( ) . StringVar ( & sourceHelmArgs . certFile , "cert-file" , "" , "TLS authentication cert file path" )
createSourceHelmCmd . Flags ( ) . StringVar ( & sourceHelmArgs . keyFile , "key-file" , "" , "TLS authentication key file path" )
createSourceHelmCmd . Flags ( ) . StringVar ( & sourceHelmArgs . caFile , "ca-file" , "" , "TLS authentication CA file path" )
createSourceHelmCmd . Flags ( ) . StringVarP ( & sourceHelmArgs . secretRef , "secret-ref" , "" , "" , "the name of an existing secret containing TLS or basic auth credentials" )
createSourceCmd . AddCommand ( createSourceHelmCmd )
}
func createSourceHelmCmdRun ( cmd * cobra . Command , args [ ] string ) error {
if len ( args ) < 1 {
return fmt . Errorf ( "HelmRepository source name is required" )
}
name := args [ 0 ]
if sourceHelmArgs . url == "" {
return fmt . Errorf ( "url is required" )
}
sourceLabels , err := parseLabels ( )
if err != nil {
return err
}
tmpDir , err := ioutil . TempDir ( "" , name )
if err != nil {
return err
}
defer os . RemoveAll ( tmpDir )
if _ , err := url . Parse ( sourceHelmArgs . url ) ; err != nil {
return fmt . Errorf ( "url parse failed: %w" , err )
}
helmRepository := & sourcev1 . HelmRepository {
ObjectMeta : metav1 . ObjectMeta {
Name : name ,
Namespace : rootArgs . namespace ,
Labels : sourceLabels ,
} ,
Spec : sourcev1 . HelmRepositorySpec {
URL : sourceHelmArgs . url ,
Interval : metav1 . Duration {
Duration : createArgs . interval ,
} ,
} ,
}
if sourceHelmArgs . secretRef != "" {
helmRepository . Spec . SecretRef = & corev1 . LocalObjectReference {
Name : sourceHelmArgs . secretRef ,
}
}
if createArgs . export {
return exportHelmRepository ( * helmRepository )
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , rootArgs . timeout )
defer cancel ( )
kubeClient , err := utils . KubeClient ( rootArgs . kubeconfig , rootArgs . kubecontext )
if err != nil {
return err
}
logger . Generatef ( "generating HelmRepository source" )
if sourceHelmArgs . secretRef == "" {
secretName := fmt . Sprintf ( "helm-%s" , name )
secret := corev1 . Secret {
ObjectMeta : metav1 . ObjectMeta {
Name : secretName ,
Namespace : rootArgs . namespace ,
Labels : sourceLabels ,
} ,
StringData : map [ string ] string { } ,
}
if sourceHelmArgs . username != "" && sourceHelmArgs . password != "" {
secret . StringData [ "username" ] = sourceHelmArgs . username
secret . StringData [ "password" ] = sourceHelmArgs . password
}
if sourceHelmArgs . certFile != "" && sourceHelmArgs . keyFile != "" {
cert , err := ioutil . ReadFile ( sourceHelmArgs . certFile )
if err != nil {
return fmt . Errorf ( "failed to read repository cert file '%s': %w" , sourceHelmArgs . certFile , err )
}
secret . StringData [ "certFile" ] = string ( cert )
key , err := ioutil . ReadFile ( sourceHelmArgs . keyFile )
if err != nil {
return fmt . Errorf ( "failed to read repository key file '%s': %w" , sourceHelmArgs . keyFile , err )
}
secret . StringData [ "keyFile" ] = string ( key )
}
if sourceHelmArgs . caFile != "" {
ca , err := ioutil . ReadFile ( sourceHelmArgs . caFile )
if err != nil {
return fmt . Errorf ( "failed to read repository CA file '%s': %w" , sourceHelmArgs . caFile , err )
}
secret . StringData [ "caFile" ] = string ( ca )
}
if len ( secret . StringData ) > 0 {
logger . Actionf ( "applying secret with repository credentials" )
if err := upsertSecret ( ctx , kubeClient , secret ) ; err != nil {
return err
}
helmRepository . Spec . SecretRef = & corev1 . LocalObjectReference {
Name : secretName ,
}
logger . Successf ( "authentication configured" )
}
}
logger . Actionf ( "applying HelmRepository source" )
namespacedName , err := upsertHelmRepository ( ctx , kubeClient , helmRepository )
if err != nil {
return err
}
logger . Waitingf ( "waiting for HelmRepository source reconciliation" )
if err := wait . PollImmediate ( rootArgs . pollInterval , rootArgs . timeout ,
isHelmRepositoryReady ( ctx , kubeClient , namespacedName , helmRepository ) ) ; err != nil {
return err
}
logger . Successf ( "HelmRepository source reconciliation completed" )
if helmRepository . Status . Artifact == nil {
return fmt . Errorf ( "HelmRepository source reconciliation completed but no artifact was found" )
}
logger . Successf ( "fetched revision: %s" , helmRepository . Status . Artifact . Revision )
return nil
}
func upsertHelmRepository ( ctx context . Context , kubeClient client . Client ,
helmRepository * sourcev1 . HelmRepository ) ( types . NamespacedName , error ) {
namespacedName := types . NamespacedName {
Namespace : helmRepository . GetNamespace ( ) ,
Name : helmRepository . GetName ( ) ,
}
var existing sourcev1 . HelmRepository
err := kubeClient . Get ( ctx , namespacedName , & existing )
if err != nil {
if errors . IsNotFound ( err ) {
if err := kubeClient . Create ( ctx , helmRepository ) ; err != nil {
return namespacedName , err
} else {
logger . Successf ( "source created" )
return namespacedName , nil
}
}
return namespacedName , err
}
existing . Labels = helmRepository . Labels
existing . Spec = helmRepository . Spec
if err := kubeClient . Update ( ctx , & existing ) ; err != nil {
return namespacedName , err
}
helmRepository = & existing
logger . Successf ( "source updated" )
return namespacedName , nil
}
func isHelmRepositoryReady ( ctx context . Context , kubeClient client . Client ,
namespacedName types . NamespacedName , helmRepository * sourcev1 . HelmRepository ) wait . ConditionFunc {
return func ( ) ( bool , error ) {
err := kubeClient . Get ( ctx , namespacedName , helmRepository )
if err != nil {
return false , err
}
// Confirm the state we are observing is for the current generation
if helmRepository . Generation != helmRepository . Status . ObservedGeneration {
return false , nil
}
if c := apimeta . FindStatusCondition ( helmRepository . Status . Conditions , meta . ReadyCondition ) ; c != nil {
switch c . Status {
case metav1 . ConditionTrue :
return true , nil
case metav1 . ConditionFalse :
return false , fmt . Errorf ( c . Message )
}
}
return false , nil
}
}