diff --git a/Makefile b/Makefile index 5d959ab2f1e4f..2326e297437a8 100644 --- a/Makefile +++ b/Makefile @@ -71,8 +71,8 @@ controller: .PHONY: controller-image controller-image: - docker build --build-arg BINARY=argocd-application-controller --build-arg MAKE_TARGET=controller -t $(IMAGE_PREFIX)argocd-controller:$(IMAGE_TAG) -f Dockerfile-argocd . - @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-controller:$(IMAGE_TAG) ; fi + docker build --build-arg BINARY=argocd-application-controller --build-arg MAKE_TARGET=controller -t $(IMAGE_PREFIX)argocd-application-controller:$(IMAGE_TAG) -f Dockerfile-argocd . + @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-application-controller:$(IMAGE_TAG) ; fi .PHONY: lint lint: diff --git a/cmd/argocd/commands/common.go b/cmd/argocd/commands/common.go index 80a1ebbe07692..1523f73936672 100644 --- a/cmd/argocd/commands/common.go +++ b/cmd/argocd/commands/common.go @@ -4,7 +4,6 @@ import ( "log" "os" - "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ) @@ -13,13 +12,6 @@ const ( cliName = "argocd" ) -var ( -// Parts of the image for installation -// These values may be overridden by the link flags during build -//imageNamespace = "argoproj" -//imageTag = "latest" -) - // GetKubeConfig creates new kubernetes client config using specified config path and config overrides variables func GetKubeConfig(configPath string, overrides clientcmd.ConfigOverrides) *rest.Config { loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() @@ -33,13 +25,3 @@ func GetKubeConfig(configPath string, overrides clientcmd.ConfigOverrides) *rest } return restConfig } - -// GetKubeClient creates new kubernetes client using specified config path and config overrides variables -func GetKubeClient(configPath string, overrides clientcmd.ConfigOverrides) *kubernetes.Clientset { - restConfig := GetKubeConfig(configPath, overrides) - clientset, err := kubernetes.NewForConfig(restConfig) - if err != nil { - log.Fatal(err) - } - return clientset -} diff --git a/cmd/argocd/commands/install.go b/cmd/argocd/commands/install.go index eae3a277adf05..852963e1396ea 100644 --- a/cmd/argocd/commands/install.go +++ b/cmd/argocd/commands/install.go @@ -1,135 +1,44 @@ package commands import ( - "fmt" - "time" - - "github.com/argoproj/argo-cd/pkg/apis/application" - appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" - "github.com/ghodss/yaml" - log "github.com/sirupsen/logrus" + "github.com/argoproj/argo-cd/common" "github.com/spf13/cobra" - apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" - apierr "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" ) -// InstallFlags has all the required parameters for installing Argo CD. -type InstallFlags struct { - DryRun bool // --dry-run -} +var ( + // These values may be overridden by the link flags during build + // (e.g. imageTag will use the official release tag on tagged builds) + imageNamespace = "argoproj" + imageTag = "latest" + + // These are the default image names which `argo install` uses during install + DefaultControllerImage = imageNamespace + "/argocd-application-controller:" + imageTag +) // NewInstallCommand returns a new instance of `argocd install` command func NewInstallCommand(globalArgs *globalFlags) *cobra.Command { var ( - installArgs InstallFlags + installParams common.InstallParameters ) var command = &cobra.Command{ Use: "install", Short: "Install the argocd components", Long: "Install the argocd components", Run: func(c *cobra.Command, args []string) { - extensionsClient := apiextensionsclient.NewForConfigOrDie(GetKubeConfig(globalArgs.kubeConfigPath, globalArgs.kubeConfigOverrides)) - installAppCRD(extensionsClient, installArgs) - installClusterCRD(extensionsClient, installArgs) + conf := GetKubeConfig(globalArgs.kubeConfigPath, globalArgs.kubeConfigOverrides) + extensionsClient := apiextensionsclient.NewForConfigOrDie(conf) + kubeClient := kubernetes.NewForConfigOrDie(conf) + common.NewInstaller(extensionsClient, kubeClient).Install(installParams) }, } - command.Flags().BoolVar(&installArgs.DryRun, "dry-run", false, "print the kubernetes manifests to stdout instead of installing") + command.Flags().BoolVar(&installParams.Upgrade, "upgrade", false, "upgrade controller/ui deployments and configmap if already installed") + command.Flags().BoolVar(&installParams.DryRun, "dry-run", false, "print the kubernetes manifests to stdout instead of installing") + command.Flags().StringVar(&installParams.Namespace, "install-namespace", common.DefaultControllerNamespace, "install into a specific Namespace") + command.Flags().StringVar(&installParams.ControllerName, "controller-name", common.DefaultControllerDeploymentName, "name of controller deployment") + command.Flags().StringVar(&installParams.ControllerImage, "controller-image", DefaultControllerImage, "use a specified controller image") + command.Flags().StringVar(&installParams.ServiceAccount, "service-account", "", "use a specified service account for the workflow-controller deployment") return command } - -func installAppCRD(extensionsClient *apiextensionsclient.Clientset, args InstallFlags) { - applicationCRD := apiextensionsv1beta1.CustomResourceDefinition{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "apiextensions.k8s.io/v1alpha1", - Kind: "CustomResourceDefinition", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: application.ApplicationFullName, - }, - Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ - Group: application.Group, - Version: appv1.SchemeGroupVersion.Version, - Scope: apiextensionsv1beta1.NamespaceScoped, - Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ - Plural: application.ApplicationPlural, - Kind: application.ApplicationKind, - ShortNames: []string{application.ApplicationShortName}, - }, - }, - } - createCRDHelper(extensionsClient, &applicationCRD, args.DryRun) -} - -func installClusterCRD(extensionsClient *apiextensionsclient.Clientset, args InstallFlags) { - clusterCRD := apiextensionsv1beta1.CustomResourceDefinition{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "apiextensions.k8s.io/v1alpha1", - Kind: "CustomResourceDefinition", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: application.ClusterFullName, - }, - Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ - Group: application.Group, - Version: appv1.SchemeGroupVersion.Version, - Scope: apiextensionsv1beta1.NamespaceScoped, - Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ - Plural: application.ClusterPlural, - Kind: application.ClusterKind, - ShortNames: []string{application.ClusterShortName}, - }, - }, - } - createCRDHelper(extensionsClient, &clusterCRD, args.DryRun) -} - -func createCRDHelper(extensionsClient *apiextensionsclient.Clientset, crd *apiextensionsv1beta1.CustomResourceDefinition, dryRun bool) { - if dryRun { - printYAML(crd) - return - } - _, err := extensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd) - if err != nil { - if !apierr.IsAlreadyExists(err) { - log.Fatalf("Failed to create CustomResourceDefinition: %v", err) - } - fmt.Printf("CustomResourceDefinition '%s' already exists\n", crd.ObjectMeta.Name) - } else { - fmt.Printf("CustomResourceDefinition '%s' created", crd.ObjectMeta.Name) - } - // wait for CRD being established - err = wait.Poll(500*time.Millisecond, 60*time.Second, func() (bool, error) { - getCrd, err := extensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.ObjectMeta.Name, metav1.GetOptions{}) - if err != nil { - return false, err - } - for _, cond := range getCrd.Status.Conditions { - switch cond.Type { - case apiextensionsv1beta1.Established: - if cond.Status == apiextensionsv1beta1.ConditionTrue { - return true, err - } - case apiextensionsv1beta1.NamesAccepted: - if cond.Status == apiextensionsv1beta1.ConditionFalse { - log.Errorf("Name conflict: %v", cond.Reason) - } - } - } - return false, err - }) - if err != nil { - log.Fatalf("Failed to wait for CustomResourceDefinition: %v", err) - } -} - -func printYAML(obj interface{}) { - objBytes, err := yaml.Marshal(obj) - if err != nil { - log.Fatalf("Failed to marshal %v", obj) - } - fmt.Printf("---\n%s\n", string(objBytes)) -} diff --git a/common/common.go b/common/common.go index 454197e4c833a..7676551916ba5 100644 --- a/common/common.go +++ b/common/common.go @@ -6,6 +6,10 @@ const ( // SecretTypeRepository indicates the data type which argocd stores as a k8s secret SecretTypeRepository = "repository" + // DefaultControllerDeploymentName is the default deployment name of the applicaiton controller + DefaultControllerDeploymentName = "application-controller" + // DefaultControllerNamespace is the default namespace where the applicaiton controller is installed + DefaultControllerNamespace = "kube-system" ) var ( diff --git a/common/installer.go b/common/installer.go new file mode 100644 index 0000000000000..958c88dadf203 --- /dev/null +++ b/common/installer.go @@ -0,0 +1,248 @@ +package common + +import ( + "fmt" + "reflect" + "time" + + "github.com/argoproj/argo-cd/pkg/apis/application" + appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" + "github.com/ghodss/yaml" + log "github.com/sirupsen/logrus" + appsv1beta2 "k8s.io/api/apps/v1beta2" + apiv1 "k8s.io/api/core/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + apierr "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" +) + +// InstallParameters has all the required parameters for installing ArgoCD. +type InstallParameters struct { + Upgrade bool + DryRun bool + Namespace string + ControllerName string + ControllerImage string + ServiceAccount string +} + +// Installer allows to install ArgoCD resources. +type Installer struct { + extensionsClient *apiextensionsclient.Clientset + clientset *kubernetes.Clientset +} + +// Install performs installation +func (installer *Installer) Install(parameters InstallParameters) { + installer.installAppCRD(parameters.DryRun) + installer.installClusterCRD(parameters.DryRun) + installer.installController(parameters) +} + +// NewInstaller creates new instance of Installer +func NewInstaller(extensionsClient *apiextensionsclient.Clientset, clientset *kubernetes.Clientset) *Installer { + return &Installer{ + extensionsClient: extensionsClient, + clientset: clientset, + } +} + +func (installer *Installer) installAppCRD(dryRun bool) { + applicationCRD := apiextensionsv1beta1.CustomResourceDefinition{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apiextensions.k8s.io/v1alpha1", + Kind: "CustomResourceDefinition", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: application.ApplicationFullName, + }, + Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ + Group: application.Group, + Version: appv1.SchemeGroupVersion.Version, + Scope: apiextensionsv1beta1.NamespaceScoped, + Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ + Plural: application.ApplicationPlural, + Kind: application.ApplicationKind, + ShortNames: []string{application.ApplicationShortName}, + }, + }, + } + installer.createCRDHelper(&applicationCRD, dryRun) +} + +func (installer *Installer) installClusterCRD(dryRun bool) { + clusterCRD := apiextensionsv1beta1.CustomResourceDefinition{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apiextensions.k8s.io/v1alpha1", + Kind: "CustomResourceDefinition", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: application.ClusterFullName, + }, + Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ + Group: application.Group, + Version: appv1.SchemeGroupVersion.Version, + Scope: apiextensionsv1beta1.NamespaceScoped, + Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ + Plural: application.ClusterPlural, + Kind: application.ClusterKind, + ShortNames: []string{application.ClusterShortName}, + }, + }, + } + installer.createCRDHelper(&clusterCRD, dryRun) +} + +func (installer *Installer) createCRDHelper(crd *apiextensionsv1beta1.CustomResourceDefinition, dryRun bool) { + if dryRun { + printYAML(crd) + return + } + _, err := installer.extensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd) + if err != nil { + if !apierr.IsAlreadyExists(err) { + log.Fatalf("Failed to create CustomResourceDefinition: %v", err) + } + fmt.Printf("CustomResourceDefinition '%s' already exists\n", crd.ObjectMeta.Name) + } else { + fmt.Printf("CustomResourceDefinition '%s' created", crd.ObjectMeta.Name) + } + // wait for CRD being established + err = wait.Poll(500*time.Millisecond, 60*time.Second, func() (bool, error) { + getCrd, err := installer.extensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.ObjectMeta.Name, metav1.GetOptions{}) + if err != nil { + return false, err + } + for _, cond := range getCrd.Status.Conditions { + switch cond.Type { + case apiextensionsv1beta1.Established: + if cond.Status == apiextensionsv1beta1.ConditionTrue { + return true, err + } + case apiextensionsv1beta1.NamesAccepted: + if cond.Status == apiextensionsv1beta1.ConditionFalse { + log.Errorf("Name conflict: %v", cond.Reason) + } + } + } + return false, err + }) + if err != nil { + log.Fatalf("Failed to wait for CustomResourceDefinition: %v", err) + } +} + +func (installer *Installer) installController(args InstallParameters) { + controllerDeployment := appsv1beta2.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps/v1beta2", + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: args.ControllerName, + Namespace: args.Namespace, + }, + Spec: appsv1beta2.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": args.ControllerName, + }, + }, + Template: apiv1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": args.ControllerName, + }, + }, + Spec: apiv1.PodSpec{ + ServiceAccountName: args.ServiceAccount, + Containers: []apiv1.Container{ + { + Name: args.ControllerName, + Image: args.ControllerImage, + Command: []string{"argocd-application-controller"}, + }, + }, + }, + }, + }, + } + installer.createDeploymentHelper(&controllerDeployment, args) +} + +// createDeploymentHelper is helper to create or update an existing deployment (if --upgrade was supplied) +func (installer *Installer) createDeploymentHelper(deployment *appsv1beta2.Deployment, args InstallParameters) { + depClient := installer.clientset.AppsV1beta2().Deployments(args.Namespace) + var result *appsv1beta2.Deployment + var err error + if args.DryRun { + printYAML(deployment) + return + } + result, err = depClient.Create(deployment) + if err != nil { + if !apierr.IsAlreadyExists(err) { + log.Fatal(err) + } + // deployment already exists + existing, err := depClient.Get(deployment.ObjectMeta.Name, metav1.GetOptions{}) + if err != nil { + log.Fatalf("Failed to get existing deployment: %v", err) + } + if upgradeNeeded(deployment, existing) { + if !args.Upgrade { + log.Fatalf("Deployment '%s' requires upgrade. Rerun with --upgrade to upgrade the deployment", deployment.ObjectMeta.Name) + } + existing, err = depClient.Update(deployment) + if err != nil { + log.Fatalf("Failed to update deployment: %v", err) + } + fmt.Printf("Existing deployment '%s' updated\n", existing.GetObjectMeta().GetName()) + } else { + fmt.Printf("Existing deployment '%s' up-to-date\n", existing.GetObjectMeta().GetName()) + } + } else { + fmt.Printf("Deployment '%s' created\n", result.GetObjectMeta().GetName()) + } +} + +// upgradeNeeded checks two deployments and returns whether or not there are obvious +// differences in a few deployment/container spec fields that would warrant an +// upgrade. WARNING: This is not intended to be comprehensive -- its primary purpose +// is to check if the controller/UI image is out of date with this version of argo. +func upgradeNeeded(dep1, dep2 *appsv1beta2.Deployment) bool { + if len(dep1.Spec.Template.Spec.Containers) != len(dep2.Spec.Template.Spec.Containers) { + return true + } + for i := 0; i < len(dep1.Spec.Template.Spec.Containers); i++ { + ctr1 := dep1.Spec.Template.Spec.Containers[i] + ctr2 := dep2.Spec.Template.Spec.Containers[i] + if ctr1.Name != ctr2.Name { + return true + } + if ctr1.Image != ctr2.Image { + return true + } + if !reflect.DeepEqual(ctr1.Env, ctr2.Env) { + return true + } + if !reflect.DeepEqual(ctr1.Command, ctr2.Command) { + return true + } + if !reflect.DeepEqual(ctr1.Args, ctr2.Args) { + return true + } + } + return false +} + +func printYAML(obj interface{}) { + objBytes, err := yaml.Marshal(obj) + if err != nil { + log.Fatalf("Failed to marshal %v", obj) + } + fmt.Printf("---\n%s\n", string(objBytes)) +}