diff --git a/api/v1alpha1/localbuild_types.go b/api/v1alpha1/localbuild_types.go index 4947ec83..227cb6d6 100644 --- a/api/v1alpha1/localbuild_types.go +++ b/api/v1alpha1/localbuild_types.go @@ -20,7 +20,14 @@ type EmbeddedArgoApplicationsPackageConfigSpec struct { Enabled bool `json:"enabled,omitempty"` } +// GitConfigSpec controls what git server to use for the idpbuilder +// It can take on the values of either gitea or gitserver +type GitConfigSpec struct { + Type string `json:"type,omitempty"` +} + type PackageConfigsSpec struct { + GitConfig GitConfigSpec `json:"gitConfig,omitempty"` Argo ArgoPackageConfigSpec `json:"argoPackageConfigs,omitempty"` EmbeddedArgoApplications EmbeddedArgoApplicationsPackageConfigSpec `json:"embeddedArgoApplicationsPackageConfigs,omitempty"` } @@ -34,10 +41,12 @@ type LocalbuildStatus struct { // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` - GitServerAvailable bool `json:"gitServerAvailable,omitempty"` - ArgoAvailable bool `json:"argoAvailable,omitempty"` - NginxAvailable bool `json:"nginxAvailable,omitempty"` - ArgoAppsCreated bool `json:"argoAppsCreated,omitempty"` + GitServerAvailable bool `json:"gitServerAvailable,omitempty"` + ArgoAvailable bool `json:"argoAvailable,omitempty"` + NginxAvailable bool `json:"nginxAvailable,omitempty"` + GiteaAvailable bool `json:"giteaAvailable,omitempty"` + ArgoAppsCreated bool `json:"argoAppsCreated,omitempty"` + GiteaSecretName string `json:"giteaSecret,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index bc5b1c71..53926bc5 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -55,6 +55,21 @@ func (in *EmbeddedArgoApplicationsPackageConfigSpec) DeepCopy() *EmbeddedArgoApp return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitConfigSpec) DeepCopyInto(out *GitConfigSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitConfigSpec. +func (in *GitConfigSpec) DeepCopy() *GitConfigSpec { + if in == nil { + return nil + } + out := new(GitConfigSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitServer) DeepCopyInto(out *GitServer) { *out = *in @@ -253,6 +268,7 @@ func (in *LocalbuildStatus) DeepCopy() *LocalbuildStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PackageConfigsSpec) DeepCopyInto(out *PackageConfigsSpec) { *out = *in + out.GitConfig = in.GitConfig out.Argo = in.Argo out.EmbeddedArgoApplications = in.EmbeddedArgoApplications } diff --git a/globals/project.go b/globals/project.go index 2c375823..4a63c761 100644 --- a/globals/project.go +++ b/globals/project.go @@ -3,7 +3,17 @@ package globals import "fmt" const ProjectName string = "idpbuilder" +const giteaResourceName string = "gitea" +const gitServerResourceName string = "gitserver" func GetProjectNamespace(name string) string { return fmt.Sprintf("%s-%s", ProjectName, name) } + +func GiteaResourceName() string { + return giteaResourceName +} + +func GitServerResourcename() string { + return gitServerResourceName +} diff --git a/pkg/build/build.go b/pkg/build/build.go index 76f01236..6f0c7bc9 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -4,6 +4,7 @@ import ( "context" "github.com/cnoe-io/idpbuilder/api/v1alpha1" + "github.com/cnoe-io/idpbuilder/globals" "github.com/cnoe-io/idpbuilder/pkg/controllers" "github.com/cnoe-io/idpbuilder/pkg/kind" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -154,6 +155,10 @@ func (b *Build) Run(ctx context.Context, recreateCluster bool) error { EmbeddedArgoApplications: v1alpha1.EmbeddedArgoApplicationsPackageConfigSpec{ Enabled: true, }, + GitConfig: v1alpha1.GitConfigSpec{ + // hint: for the old behavior, replace Type value below with globals.GitServerResourcename() + Type: globals.GiteaResourceName(), + }, }, } return nil diff --git a/pkg/controllers/gitserver/controller.go b/pkg/controllers/gitserver/controller.go index dc112c5b..b4e2e08a 100644 --- a/pkg/controllers/gitserver/controller.go +++ b/pkg/controllers/gitserver/controller.go @@ -21,7 +21,6 @@ import ( ) const ( - gitServerResourceName string = "gitserver" gitServerDeploymentContainerName string = "httpd" gitServerIngressHostnameBase string = ".cnoe.localtest.me" repoUrlFmt string = "http://%s.%s.svc/idpbuilder-resources.git" @@ -38,7 +37,7 @@ func ingressHostname(resource *v1alpha1.GitServer) string { } func managedResourceName(resource *v1alpha1.GitServer) string { - return fmt.Sprintf("%s-%s", gitServerResourceName, resource.Name) + return fmt.Sprintf("%s-%s", globals.GitServerResourcename(), resource.Name) } type subReconciler func(ctx context.Context, req ctrl.Request, resource *v1alpha1.GitServer) (ctrl.Result, error) diff --git a/pkg/controllers/localbuild/argo.go b/pkg/controllers/localbuild/argo.go index b6545501..94e42158 100644 --- a/pkg/controllers/localbuild/argo.go +++ b/pkg/controllers/localbuild/argo.go @@ -5,105 +5,47 @@ import ( "embed" "github.com/cnoe-io/idpbuilder/api/v1alpha1" - "github.com/cnoe-io/idpbuilder/pkg/k8s" - "github.com/cnoe-io/idpbuilder/pkg/util" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/log" ) //go:embed resources/argo/* -var installFS embed.FS +var installArgoFS embed.FS const ( - argoApplicationControllerName string = "argocd-application-controller" - argoServerName string = "argocd-server" - argoRepoServerName string = "argocd-repo-server" + argocdNamespace string = "argocd" ) -func GetRawInstallResources() ([][]byte, error) { - return util.ConvertFSToBytes(installFS, "resources/argo") -} - -func GetK8sInstallResources(scheme *runtime.Scheme) ([]client.Object, error) { - rawResources, err := GetRawInstallResources() - if err != nil { - return nil, err - } - - return k8s.ConvertRawResourcesToObjects(scheme, rawResources) -} - -func newArgoNamespace() *corev1.Namespace { - return &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "argocd", - }, - } -} - func (r *LocalbuildReconciler) ReconcileArgo(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild) (ctrl.Result, error) { - log := log.FromContext(ctx) - - if !resource.Spec.PackageConfigs.Argo.Enabled { - log.Info("Argo installation disabled, skipping") - return ctrl.Result{}, nil - } - - // Install Argo - argonsClient := client.NewNamespacedClient(r.Client, "argocd") - installObjs, err := GetK8sInstallResources(r.Scheme) - if err != nil { - return ctrl.Result{}, err - } - - // Ensure namespace exists - argocdNs := newArgoNamespace() - if err = r.Client.Get(ctx, types.NamespacedName{Name: "argocd"}, argocdNs); err != nil { - // We got an error so try creating the NS - if err = r.Client.Create(ctx, argocdNs); err != nil { - return ctrl.Result{}, err - } + argocd := EmbeddedInstallation{ + name: "Argo CD", + resourcePath: "resources/argo", + resourceFS: installArgoFS, + namespace: argocdNamespace, + monitoredResources: map[string]schema.GroupVersionKind{ + "argocd-server": { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + "argocd-repo-server": { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + "argocd-application-controller": { + Group: "apps", + Version: "v1", + Kind: "StatefulSet", + }, + }, + skipReadinessCheck: true, } - log.Info("Installing argo resources") - for _, obj := range installObjs { - // Find the objects we need to track and own for status updates - if obj.GetObjectKind().GroupVersionKind().Kind == "StatefulSet" && obj.GetName() == argoApplicationControllerName { - if err = controllerutil.SetControllerReference(resource, obj, r.Scheme); err != nil { - log.Error(err, "Setting controller reference for Argo application controller") - return ctrl.Result{}, err - } - } else if obj.GetObjectKind().GroupVersionKind().Kind == "Deployment" { - switch obj.GetName() { - case argoServerName: - fallthrough - case argoRepoServerName: - gotObj := appsv1.Deployment{} - if err := r.Client.Get(ctx, types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, &gotObj); err != nil { - if err = controllerutil.SetControllerReference(resource, obj, r.Scheme); err != nil { - log.Error(err, "Setting controller reference for Argo deployment", "deployment", obj) - return ctrl.Result{}, err - } - } - } - } - - // Create object - if err = k8s.EnsureObject(ctx, argonsClient, obj, "argocd"); err != nil { - return ctrl.Result{}, err - } + if result, err := argocd.Install(ctx, req, resource, r.Client, r.Scheme); err != nil { + return result, err } - // Set Argo available. - // TODO(greghaynes) This should actually wait for status of some resources resource.Status.ArgoAvailable = true - return ctrl.Result{}, nil } diff --git a/pkg/controllers/localbuild/argo_test.go b/pkg/controllers/localbuild/argo_test.go index cda3a192..ccb6c64e 100644 --- a/pkg/controllers/localbuild/argo_test.go +++ b/pkg/controllers/localbuild/argo_test.go @@ -7,7 +7,11 @@ import ( ) func TestGetRawInstallResources(t *testing.T) { - resources, err := GetRawInstallResources() + e := EmbeddedInstallation{ + resourceFS: installArgoFS, + resourcePath: "resources/argo", + } + resources, err := e.rawInstallResources() if err != nil { t.Fatalf("GetRawInstallResources() error: %v", err) } @@ -23,7 +27,11 @@ func TestGetRawInstallResources(t *testing.T) { } func TestGetK8sInstallResources(t *testing.T) { - objs, err := GetK8sInstallResources(k8s.GetScheme()) + e := EmbeddedInstallation{ + resourceFS: installArgoFS, + resourcePath: "resources/argo", + } + objs, err := e.installResources(k8s.GetScheme()) if err != nil { t.Fatalf("GetK8sInstallResources() error: %v", err) } diff --git a/pkg/controllers/localbuild/controller.go b/pkg/controllers/localbuild/controller.go index 60851092..2ffb941d 100644 --- a/pkg/controllers/localbuild/controller.go +++ b/pkg/controllers/localbuild/controller.go @@ -23,7 +23,6 @@ import ( const ( defaultArgoCDProjectName string = "default" EmbeddedGitServerName string = "embedded" - gitServerResourceName string = "gitserver" gitServerDeploymentContainerName string = "httpd" gitServerIngressHostnameBase string = ".cnoe.localtest.me" repoUrlFmt string = "http://%s.%s.svc/idpbuilder-resources.git" @@ -40,7 +39,7 @@ func ingressHostname(resource *v1alpha1.GitServer) string { } func managedResourceName(resource *v1alpha1.GitServer) string { - return fmt.Sprintf("%s-%s", gitServerResourceName, resource.Name) + return fmt.Sprintf("%s-%s", globals.GitServerResourcename(), resource.Name) } type LocalbuildReconciler struct { @@ -67,12 +66,26 @@ func (r *LocalbuildReconciler) Reconcile(ctx context.Context, req ctrl.Request) // Make sure we post process defer r.postProcessReconcile(ctx, req, &localBuild) + // respecting order of installation matters as there are hard dependencies subReconcilers := []subReconciler{ r.ReconcileProjectNamespace, - r.ReconcileArgo, r.ReconcileNginx, - r.ReconcileEmbeddedGitServer, - r.ReconcileArgoApps, + r.ReconcileArgo, + } + + switch localBuild.Spec.PackageConfigs.GitConfig.Type { + case globals.GitServerResourcename(): + subReconcilers = append( + subReconcilers, + []subReconciler{r.ReconcileEmbeddedGitServer, r.ReconcileArgoAppsWithGitServer}..., + ) + case globals.GiteaResourceName(): + subReconcilers = append( + subReconcilers, + []subReconciler{r.ReconcileGitea, r.ReconcileArgoAppsWithGitea}..., + ) + default: + return ctrl.Result{}, fmt.Errorf("GitConfig %s is invalid for LocalBuild %s", localBuild.Spec.PackageConfigs.GitConfig.Type, localBuild.GetName()) } for _, sub := range subReconcilers { @@ -171,7 +184,7 @@ func (r *LocalbuildReconciler) ReconcileEmbeddedGitServer(ctx context.Context, r return ctrl.Result{}, err } -func (r *LocalbuildReconciler) ReconcileArgoApps(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild) (ctrl.Result, error) { +func (r *LocalbuildReconciler) ReconcileArgoAppsWithGitServer(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild) (ctrl.Result, error) { log := log.FromContext(ctx) // Bail if embedded argo applications not enabled @@ -263,3 +276,11 @@ func (r *LocalbuildReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&v1alpha1.Localbuild{}). Complete(r) } + +func (r *LocalbuildReconciler) ReconcileArgoAppsWithGitea(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild) (ctrl.Result, error) { + log := log.FromContext(ctx) + + log.Info("TODO(nimak): enable installing Argo Apps") + r.shouldShutdown = true + return ctrl.Result{}, nil +} diff --git a/pkg/controllers/localbuild/gitea.go b/pkg/controllers/localbuild/gitea.go new file mode 100644 index 00000000..b8ac5459 --- /dev/null +++ b/pkg/controllers/localbuild/gitea.go @@ -0,0 +1,43 @@ +package localbuild + +import ( + "context" + "embed" + + "github.com/cnoe-io/idpbuilder/api/v1alpha1" + "k8s.io/apimachinery/pkg/runtime/schema" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + giteaNamespace = "gitea" + // hardcoded secret name from what we have in the yaml installation file + giteaAdminSecret = "gitea-admin-secret" +) + +//go:embed resources/gitea/k8s/* +var installGiteaFS embed.FS + +func (r *LocalbuildReconciler) ReconcileGitea(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild) (ctrl.Result, error) { + gitea := EmbeddedInstallation{ + name: "Gitea", + resourcePath: "resources/gitea/k8s", + resourceFS: installGiteaFS, + namespace: giteaNamespace, + monitoredResources: map[string]schema.GroupVersionKind{ + "my-gitea": { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + }, + } + + if result, err := gitea.Install(ctx, req, resource, r.Client, r.Scheme); err != nil { + return result, err + } + + resource.Status.GiteaSecretName = giteaAdminSecret + resource.Status.GiteaAvailable = true + return ctrl.Result{}, nil +} diff --git a/pkg/controllers/localbuild/installer.go b/pkg/controllers/localbuild/installer.go new file mode 100644 index 00000000..74277885 --- /dev/null +++ b/pkg/controllers/localbuild/installer.go @@ -0,0 +1,181 @@ +package localbuild + +import ( + "context" + "embed" + "errors" + "fmt" + "sync" + "time" + + "github.com/cnoe-io/idpbuilder/api/v1alpha1" + "github.com/cnoe-io/idpbuilder/pkg/k8s" + "github.com/cnoe-io/idpbuilder/pkg/util" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +var timeout = time.After(5 * time.Minute) + +type EmbeddedInstallation struct { + name string + resourcePath string + namespace string + + // skips waiting on expected resources to become ready + skipReadinessCheck bool + + // name and gvk pair for resources that need to be monitored + monitoredResources map[string]schema.GroupVersionKind + + resourceFS embed.FS +} + +func (e *EmbeddedInstallation) rawInstallResources() ([][]byte, error) { + return util.ConvertFSToBytes(e.resourceFS, e.resourcePath) +} + +func (e *EmbeddedInstallation) installResources(scheme *runtime.Scheme) ([]client.Object, error) { + rawResources, err := e.rawInstallResources() + if err != nil { + return nil, err + } + + return k8s.ConvertRawResourcesToObjects(scheme, rawResources) +} + +func (e *EmbeddedInstallation) newNamespace(namespace string) *corev1.Namespace { + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } +} + +func (e *EmbeddedInstallation) Install(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild, cli client.Client, sc *runtime.Scheme) (ctrl.Result, error) { + log := log.FromContext(ctx) + + nsClient := client.NewNamespacedClient(cli, e.namespace) + installObjs, err := e.installResources(sc) + if err != nil { + return ctrl.Result{}, err + } + + // Ensure namespace exists + newNS := e.newNamespace(e.namespace) + if err = cli.Get(ctx, types.NamespacedName{Name: e.namespace}, newNS); err != nil { + // We got an error so try creating the NS + if err = cli.Create(ctx, newNS); err != nil { + return ctrl.Result{}, err + } + } + + log.Info(fmt.Sprintf("Installing/Reconciling %s resources", e.name)) + for _, obj := range installObjs { + if gvk, ok := e.monitoredResources[obj.GetName()]; ok { + if obj.GetObjectKind().GroupVersionKind() == gvk { + sch := runtime.NewScheme() + _ = appsv1.AddToScheme(sch) + if gvkObj, err := sch.New(gvk); err == nil { + if gotObj, ok := gvkObj.(client.Object); ok { + if err := cli.Get(ctx, types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, gotObj); err != nil { + if err = controllerutil.SetControllerReference(resource, obj, sc); err != nil { + log.Error(err, "Setting controller reference for deployment", obj.GetName(), obj) + return ctrl.Result{}, err + } + } + } + } + } + } + + // Create object + if err = k8s.EnsureObject(ctx, nsClient, obj, e.namespace); err != nil { + return ctrl.Result{}, err + } + } + + // return early if readiness check is disabled + if e.skipReadinessCheck { + return ctrl.Result{}, nil + } + + // wait for expected resources to become available + errCh := make(chan error) + var wg sync.WaitGroup + + for _, obj := range installObjs { + if gvk, ok := e.monitoredResources[obj.GetName()]; ok { + if obj.GetObjectKind().GroupVersionKind() != gvk { + continue + } + + wg.Add(1) + go func(obj client.Object, gvk schema.GroupVersionKind) { + defer wg.Done() + + sch := runtime.NewScheme() + _ = appsv1.AddToScheme(sch) + gvkObj, err := sch.New(gvk) + if err != nil { + errCh <- err + return + } + + for { + if gotObj, ok := gvkObj.(client.Object); ok { + if err := cli.Get(ctx, types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, gotObj); err != nil { + errCh <- err + return + } + + switch t := gotObj.(type) { + case *appsv1.Deployment: + if t.Status.AvailableReplicas >= 1 { + log.Info(t.GetName(), "deployment", t.Status.AvailableReplicas) + return + } + case *appsv1.StatefulSet: + if t.Status.AvailableReplicas >= 1 { + log.Info(t.GetName(), "statefulset", t.Status.AvailableReplicas) + return + } + } + } + + log.Info(fmt.Sprintf("Waiting for %s %s to become ready", gvk.Kind, obj.GetName())) + time.Sleep(30 * time.Second) + } + }(obj, gvk) + } + } + + go func() { + wg.Wait() + close(errCh) + }() + + select { + case <-timeout: + err := errors.New("Timeout") + log.Error(err, fmt.Sprintf("Didn't reconcile %s on time", e.name)) + return ctrl.Result{}, err + case err, errOccurred := <-errCh: + if !errOccurred { + log.Info(fmt.Sprintf("%s is ready!", e.name)) + } else { + log.Error(err, fmt.Sprintf("failed to reconcile the %s resources", e.name)) + return ctrl.Result{}, err + } + } + + return ctrl.Result{}, nil +} diff --git a/pkg/controllers/localbuild/nginx.go b/pkg/controllers/localbuild/nginx.go index 9e59fac5..dbc41ddb 100644 --- a/pkg/controllers/localbuild/nginx.go +++ b/pkg/controllers/localbuild/nginx.go @@ -3,133 +3,38 @@ package localbuild import ( "context" "embed" - "errors" - "time" "github.com/cnoe-io/idpbuilder/api/v1alpha1" - "github.com/cnoe-io/idpbuilder/pkg/k8s" - "github.com/cnoe-io/idpbuilder/pkg/util" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/log" ) const ( - nginxNamespace string = "ingress-nginx" - nginxDeployment string = "ingress-nginx-controller" + nginxNamespace string = "ingress-nginx" ) //go:embed resources/nginx/k8s/* var installNginxFS embed.FS -var timeout = time.After(3 * time.Minute) -func RawNginxInstallResources() ([][]byte, error) { - return util.ConvertFSToBytes(installNginxFS, "resources/nginx/k8s") -} - -func NginxInstallResources(scheme *runtime.Scheme) ([]client.Object, error) { - rawResources, err := RawNginxInstallResources() - if err != nil { - return nil, err - } - - return k8s.ConvertRawResourcesToObjects(scheme, rawResources) -} - -func newNginxNamespace() *corev1.Namespace { - return &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: nginxNamespace, +func (r *LocalbuildReconciler) ReconcileNginx(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild) (ctrl.Result, error) { + nginx := EmbeddedInstallation{ + name: "Nginx", + resourcePath: "resources/nginx/k8s", + resourceFS: installNginxFS, + namespace: nginxNamespace, + monitoredResources: map[string]schema.GroupVersionKind{ + "ingress-nginx-controller": { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, }, } -} - -func (r LocalbuildReconciler) ReconcileNginx(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild) (ctrl.Result, error) { - log := log.FromContext(ctx) - - nginxNSClient := client.NewNamespacedClient(r.Client, nginxNamespace) - installObjs, err := NginxInstallResources(r.Scheme) - if err != nil { - return ctrl.Result{}, err - } - - // Ensure namespace exists - nginxNewNS := newNginxNamespace() - if err = r.Client.Get(ctx, types.NamespacedName{Name: nginxNamespace}, nginxNewNS); err != nil { - // We got an error so try creating the NS - if err = r.Client.Create(ctx, nginxNewNS); err != nil { - return ctrl.Result{}, err - } - } - - log.Info("Installing/Reconciling Nginx resources") - for _, obj := range installObjs { - if obj.GetObjectKind().GroupVersionKind().Kind == "Deployment" { - switch obj.GetName() { - case nginxDeployment: - gotObj := appsv1.Deployment{} - if err := r.Client.Get(ctx, types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, &gotObj); err != nil { - if err = controllerutil.SetControllerReference(resource, obj, r.Scheme); err != nil { - log.Error(err, "Setting controller reference for Nginx deployment", "deployment", obj) - return ctrl.Result{}, err - } - } - } - } - - // Create object - if err = k8s.EnsureObject(ctx, nginxNSClient, obj, nginxNamespace); err != nil { - return ctrl.Result{}, err - } - } - - // Wait for Nginx to become available - ready := make(chan error) - go func([]client.Object) { - for { - for _, obj := range installObjs { - if obj.GetObjectKind().GroupVersionKind().Kind == "Deployment" { - switch obj.GetName() { - case nginxDeployment: - gotObj := appsv1.Deployment{} - if err := r.Client.Get(ctx, types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}, &gotObj); err != nil { - ready <- err - return - } - - if gotObj.Status.AvailableReplicas >= 1 { - close(ready) - return - } - } - } - } - log.Info("Waiting for Nginx to become ready") - time.Sleep(30 * time.Second) - } - }(installObjs) - select { - case <-timeout: - err := errors.New("Timeout") - log.Error(err, "Didn't reconcile Nginx on time.") - return ctrl.Result{}, err - case err, errOccurred := <-ready: - if !errOccurred { - log.Info("Nginx is ready!") - resource.Status.NginxAvailable = true - } else { - log.Error(err, "failed to reconcile the Nginx resources") - resource.Status.NginxAvailable = false - return ctrl.Result{}, err - } + if result, err := nginx.Install(ctx, req, resource, r.Client, r.Scheme); err != nil { + return result, err } + resource.Status.NginxAvailable = true return ctrl.Result{}, nil } diff --git a/pkg/controllers/localbuild/resources/gitea/k8s/install.yaml b/pkg/controllers/localbuild/resources/gitea/k8s/install.yaml new file mode 100644 index 00000000..bf5370e3 --- /dev/null +++ b/pkg/controllers/localbuild/resources/gitea/k8s/install.yaml @@ -0,0 +1,590 @@ +--- +# Source: gitea/templates/gitea/config.yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-gitea-inline-config + labels: + helm.sh/chart: gitea-9.5.1 + app: gitea + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: my-gitea + app.kubernetes.io/version: "1.20.5" + version: "1.20.5" + app.kubernetes.io/managed-by: Helm +type: Opaque +stringData: + _generals_: "" + cache: ADAPTER=memory + database: DB_TYPE=sqlite3 + indexer: ISSUE_INDEXER_TYPE=db + metrics: ENABLED=false + queue: TYPE=level + repository: ROOT=/data/git/gitea-repositories + security: INSTALL_LOCK=true + server: |- + APP_DATA_PATH=/data + DOMAIN=localtest.me + ENABLE_PPROF=false + HTTP_PORT=3000 + PROTOCOL=http + ROOT_URL=http://localtest.me:3000 + SSH_DOMAIN=localtest.me + SSH_LISTEN_PORT=2222 + SSH_PORT=22 + START_SSH_SERVER=true + session: |- + PROVIDER=memory + PROVIDER_CONFIG= +--- +# Source: gitea/templates/gitea/config.yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-gitea + labels: + helm.sh/chart: gitea-9.5.1 + app: gitea + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: my-gitea + app.kubernetes.io/version: "1.20.5" + version: "1.20.5" + app.kubernetes.io/managed-by: Helm +type: Opaque +stringData: + assertions: | + config_environment.sh: |- + #!/usr/bin/env bash + set -euo pipefail + + function env2ini::log() { + printf "${1}\n" + } + + function env2ini::read_config_to_env() { + local section="${1}" + local line="${2}" + + if [[ -z "${line}" ]]; then + # skip empty line + return + fi + + # 'xargs echo -n' trims all leading/trailing whitespaces and a trailing new line + local setting="$(awk -F '=' '{print $1}' <<< "${line}" | xargs echo -n)" + + if [[ -z "${setting}" ]]; then + env2ini::log ' ! invalid setting' + exit 1 + fi + + local value='' + local regex="^${setting}(\s*)=(\s*)(.*)" + if [[ $line =~ $regex ]]; then + value="${BASH_REMATCH[3]}" + else + env2ini::log ' ! invalid setting' + exit 1 + fi + + env2ini::log " + '${setting}'" + + if [[ -z "${section}" ]]; then + export "GITEA____${setting^^}=${value}" # '^^' makes the variable content uppercase + return + fi + + local masked_section="${section//./_0X2E_}" # '//' instructs to replace all matches + masked_section="${masked_section//-/_0X2D_}" + + export "GITEA__${masked_section^^}__${setting^^}=${value}" # '^^' makes the variable content uppercase + } + + function env2ini::reload_preset_envs() { + env2ini::log "Reloading preset envs..." + + while read -r line; do + if [[ -z "${line}" ]]; then + # skip empty line + return + fi + + # 'xargs echo -n' trims all leading/trailing whitespaces and a trailing new line + local setting="$(awk -F '=' '{print $1}' <<< "${line}" | xargs echo -n)" + + if [[ -z "${setting}" ]]; then + env2ini::log ' ! invalid setting' + exit 1 + fi + + local value='' + local regex="^${setting}(\s*)=(\s*)(.*)" + if [[ $line =~ $regex ]]; then + value="${BASH_REMATCH[3]}" + else + env2ini::log ' ! invalid setting' + exit 1 + fi + + env2ini::log " + '${setting}'" + + export "${setting^^}=${value}" # '^^' makes the variable content uppercase + done < "/tmp/existing-envs" + + rm /tmp/existing-envs + } + + + function env2ini::process_config_file() { + local config_file="${1}" + local section="$(basename "${config_file}")" + + if [[ $section == '_generals_' ]]; then + env2ini::log " [ini root]" + section='' + else + env2ini::log " ${section}" + fi + + while read -r line; do + env2ini::read_config_to_env "${section}" "${line}" + done < <(awk 1 "${config_file}") # Helm .toYaml trims the trailing new line which breaks line processing; awk 1 ... adds it back while reading + } + + function env2ini::load_config_sources() { + local path="${1}" + + if [[ -d "${path}" ]]; then + env2ini::log "Processing $(basename "${path}")..." + + while read -d '' configFile; do + env2ini::process_config_file "${configFile}" + done < <(find "${path}" -type l -not -name '..data' -print0) + + env2ini::log "\n" + fi + } + + function env2ini::generate_initial_secrets() { + # These environment variables will either be + # - overwritten with user defined values, + # - initially used to set up Gitea + # Anyway, they won't harm existing app.ini files + + export GITEA__SECURITY__INTERNAL_TOKEN=$(gitea generate secret INTERNAL_TOKEN) + export GITEA__SECURITY__SECRET_KEY=$(gitea generate secret SECRET_KEY) + export GITEA__OAUTH2__JWT_SECRET=$(gitea generate secret JWT_SECRET) + export GITEA__SERVER__LFS_JWT_SECRET=$(gitea generate secret LFS_JWT_SECRET) + + env2ini::log "...Initial secrets generated\n" + } + + # save existing envs prior to script execution. Necessary to keep order of preexisting and custom envs + env | (grep GITEA || [[ $? == 1 ]]) > /tmp/existing-envs + + # MUST BE CALLED BEFORE OTHER CONFIGURATION + env2ini::generate_initial_secrets + + env2ini::load_config_sources '/env-to-ini-mounts/inlines/' + env2ini::load_config_sources '/env-to-ini-mounts/additionals/' + + # load existing envs to override auto generated envs + env2ini::reload_preset_envs + + env2ini::log "=== All configuration sources loaded ===\n" + + # safety to prevent rewrite of secret keys if an app.ini already exists + if [ -f ${GITEA_APP_INI} ]; then + env2ini::log 'An app.ini file already exists. To prevent overwriting secret keys, these settings are dropped and remain unchanged:' + env2ini::log ' - security.INTERNAL_TOKEN' + env2ini::log ' - security.SECRET_KEY' + env2ini::log ' - oauth2.JWT_SECRET' + env2ini::log ' - server.LFS_JWT_SECRET' + + unset GITEA__SECURITY__INTERNAL_TOKEN + unset GITEA__SECURITY__SECRET_KEY + unset GITEA__OAUTH2__JWT_SECRET + unset GITEA__SERVER__LFS_JWT_SECRET + fi + + environment-to-ini -o $GITEA_APP_INI +--- +# Source: gitea/templates/gitea/init.yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-gitea-init + labels: + helm.sh/chart: gitea-9.5.1 + app: gitea + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: my-gitea + app.kubernetes.io/version: "1.20.5" + version: "1.20.5" + app.kubernetes.io/managed-by: Helm +type: Opaque +stringData: + configure_gpg_environment.sh: |- + #!/usr/bin/env bash + set -eu + + gpg --batch --import /raw/private.asc + init_directory_structure.sh: |- + #!/usr/bin/env bash + + set -euo pipefail + + set -x + mkdir -p /data/git/.ssh + chmod -R 700 /data/git/.ssh + [ ! -d /data/gitea/conf ] && mkdir -p /data/gitea/conf + + # prepare temp directory structure + mkdir -p "${GITEA_TEMP}" + chmod ug+rwx "${GITEA_TEMP}" + + + + configure_gitea.sh: |- + #!/usr/bin/env bash + + set -euo pipefail + + echo '==== BEGIN GITEA CONFIGURATION ====' + + { # try + gitea migrate + } || { # catch + echo "Gitea migrate might fail due to database connection...This init-container will try again in a few seconds" + exit 1 + } + function configure_admin_user() { + local ACCOUNT_ID=$(gitea admin user list --admin | grep -e "\s\+${GITEA_ADMIN_USERNAME}\s\+" | awk -F " " "{printf \$1}") + if [[ -z "${ACCOUNT_ID}" ]]; then + echo "No admin user '${GITEA_ADMIN_USERNAME}' found. Creating now..." + gitea admin user create --admin --username "${GITEA_ADMIN_USERNAME}" --password "${GITEA_ADMIN_PASSWORD}" --email "gitea@local.domain" --must-change-password=false + echo '...created.' + else + echo "Admin account '${GITEA_ADMIN_USERNAME}' already exist. Running update to sync password..." + gitea admin user change-password --username "${GITEA_ADMIN_USERNAME}" --password "${GITEA_ADMIN_PASSWORD}" + echo '...password sync done.' + fi + } + + configure_admin_user + + function configure_ldap() { + echo 'no ldap configuration... skipping.' + } + + configure_ldap + + function configure_oauth() { + echo 'no oauth configuration... skipping.' + } + + configure_oauth + + echo '==== END GITEA CONFIGURATION ====' +--- +# Source: gitea/templates/gitea/http-svc.yaml +apiVersion: v1 +kind: Service +metadata: + name: my-gitea-http + labels: + helm.sh/chart: gitea-9.5.1 + app: gitea + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: my-gitea + app.kubernetes.io/version: "1.20.5" + version: "1.20.5" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: ClusterIP + clusterIP: None + ports: + - name: http + port: 3000 + targetPort: + selector: + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: my-gitea +--- +# Source: gitea/templates/gitea/ssh-svc.yaml +apiVersion: v1 +kind: Service +metadata: + name: my-gitea-ssh + labels: + helm.sh/chart: gitea-9.5.1 + app: gitea + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: my-gitea + app.kubernetes.io/version: "1.20.5" + version: "1.20.5" + app.kubernetes.io/managed-by: Helm + annotations: + {} +spec: + type: NodePort + externalTrafficPolicy: Local + ports: + - name: ssh + port: 22 + targetPort: 2222 + protocol: TCP + nodePort: 32222 + selector: + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: my-gitea +--- +# Source: gitea/templates/gitea/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-gitea + annotations: + labels: + helm.sh/chart: gitea-9.5.1 + app: gitea + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: my-gitea + app.kubernetes.io/version: "1.20.5" + version: "1.20.5" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 100% + selector: + matchLabels: + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: my-gitea + template: + metadata: + annotations: + checksum/config: b72b51d66dcd6dd355217fae8f65b19eae7fe343092e672ab2b117f2fd35638c + labels: + helm.sh/chart: gitea-9.5.1 + app: gitea + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: my-gitea + app.kubernetes.io/version: "1.20.5" + version: "1.20.5" + app.kubernetes.io/managed-by: Helm + spec: + + securityContext: + fsGroup: 1000 + initContainers: + - name: init-directories + image: "gitea/gitea:1.20.5-rootless" + imagePullPolicy: Always + command: ["/usr/sbin/init_directory_structure.sh"] + env: + - name: GITEA_APP_INI + value: /data/gitea/conf/app.ini + - name: GITEA_CUSTOM + value: /data/gitea + - name: GITEA_WORK_DIR + value: /data + - name: GITEA_TEMP + value: /tmp/gitea + volumeMounts: + - name: init + mountPath: /usr/sbin + - name: temp + mountPath: /tmp + - name: data + mountPath: /data + + securityContext: + {} + resources: + limits: {} + requests: + cpu: 100m + memory: 128Mi + - name: init-app-ini + image: "gitea/gitea:1.20.5-rootless" + imagePullPolicy: Always + command: ["/usr/sbin/config_environment.sh"] + env: + - name: GITEA_APP_INI + value: /data/gitea/conf/app.ini + - name: GITEA_CUSTOM + value: /data/gitea + - name: GITEA_WORK_DIR + value: /data + - name: GITEA_TEMP + value: /tmp/gitea + volumeMounts: + - name: config + mountPath: /usr/sbin + - name: temp + mountPath: /tmp + - name: data + mountPath: /data + - name: inline-config-sources + mountPath: /env-to-ini-mounts/inlines/ + + securityContext: + {} + resources: + limits: {} + requests: + cpu: 100m + memory: 128Mi + - name: configure-gitea + image: "gitea/gitea:1.20.5-rootless" + command: ["/usr/sbin/configure_gitea.sh"] + imagePullPolicy: Always + securityContext: + runAsUser: 1000 + env: + - name: GITEA_APP_INI + value: /data/gitea/conf/app.ini + - name: GITEA_CUSTOM + value: /data/gitea + - name: GITEA_WORK_DIR + value: /data + - name: GITEA_TEMP + value: /tmp/gitea + - name: HOME + value: /data/gitea/git + - name: GITEA_ADMIN_USERNAME + valueFrom: + secretKeyRef: + key: username + name: gitea-admin-secret + - name: GITEA_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + key: password + name: gitea-admin-secret + volumeMounts: + - name: init + mountPath: /usr/sbin + - name: temp + mountPath: /tmp + - name: data + mountPath: /data + + resources: + limits: {} + requests: + cpu: 100m + memory: 128Mi + terminationGracePeriodSeconds: 60 + containers: + - name: gitea + image: "gitea/gitea:1.20.5-rootless" + imagePullPolicy: Always + env: + # SSH Port values have to be set here as well for openssh configuration + - name: SSH_LISTEN_PORT + value: "2222" + - name: SSH_PORT + value: "22" + - name: GITEA_APP_INI + value: /data/gitea/conf/app.ini + - name: GITEA_CUSTOM + value: /data/gitea + - name: GITEA_WORK_DIR + value: /data + - name: GITEA_TEMP + value: /tmp/gitea + - name: TMPDIR + value: /tmp/gitea + - name: HOME + value: /data/gitea/git + ports: + - name: ssh + containerPort: 2222 + - name: http + containerPort: 3000 + livenessProbe: + failureThreshold: 10 + initialDelaySeconds: 200 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: http + timeoutSeconds: 1 + readinessProbe: + failureThreshold: 3 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: http + timeoutSeconds: 1 + resources: + {} + securityContext: + {} + volumeMounts: + - name: temp + mountPath: /tmp + - name: data + mountPath: /data + + volumes: + - name: init + secret: + secretName: my-gitea-init + defaultMode: 110 + - name: config + secret: + secretName: my-gitea + defaultMode: 110 + - name: inline-config-sources + secret: + secretName: my-gitea-inline-config + - name: temp + emptyDir: {} + - name: data + emptyDir: {} +--- +# Source: gitea/templates/gitea/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: my-gitea + labels: + helm.sh/chart: gitea-9.5.1 + app: gitea + app.kubernetes.io/name: gitea + app.kubernetes.io/instance: my-gitea + app.kubernetes.io/version: "1.20.5" + version: "1.20.5" + app.kubernetes.io/managed-by: Helm + annotations: +spec: + ingressClassName: nginx + rules: + - host: "gitea.cnoe.localtest.me" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: my-gitea-http + port: + number: 3000 +--- +apiVersion: v1 +kind: Secret +metadata: + name: gitea-admin-secret +type: Opaque +stringData: + username: giteaAdmin + password: giteaPassword diff --git a/pkg/controllers/localbuild/resources/gitea/values.yaml b/pkg/controllers/localbuild/resources/gitea/values.yaml new file mode 100644 index 00000000..503f3211 --- /dev/null +++ b/pkg/controllers/localbuild/resources/gitea/values.yaml @@ -0,0 +1,43 @@ +redis-cluster: + enabled: false +postgresql: + enabled: false +postgresql-ha: + enabled: false + +persistence: + enabled: false + +test: + enabled: false + +gitea: + admin: + existingSecret: gitea-admin-secret + config: + database: + DB_TYPE: sqlite3 + session: + PROVIDER: memory + cache: + ADAPTER: memory + queue: + TYPE: level + server: + DOMAIN: localtest.me + ROOT_URL: 'http://localtest.me:3000' + +service: + ssh: + type: NodePort + nodePort: 32222 + externalTrafficPolicy: Local + +ingress: + enabled: true + className: nginx + hosts: + - host: gitea.cnoe.localtest.me + paths: + - path: / + pathType: Prefix \ No newline at end of file diff --git a/pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml b/pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml index 2202c0c4..af65389f 100644 --- a/pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml +++ b/pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml @@ -52,6 +52,14 @@ spec: argo applications and the associated GitServer type: boolean type: object + gitConfig: + description: GitConfigSpec controls what git server to use for + the idpbuilder It can take on the values of either gitea or + gitserver + properties: + type: + type: string + type: object type: object type: object status: @@ -62,6 +70,10 @@ spec: type: boolean gitServerAvailable: type: boolean + giteaAvailable: + type: boolean + giteaSecret: + type: string nginxAvailable: type: boolean observedGeneration: