diff --git a/globals/project.go b/globals/project.go index 4a63c761..38d385b0 100644 --- a/globals/project.go +++ b/globals/project.go @@ -2,18 +2,18 @@ package globals import "fmt" -const ProjectName string = "idpbuilder" -const giteaResourceName string = "gitea" -const gitServerResourceName string = "gitserver" +const ( + ProjectName string = "idpbuilder" -func GetProjectNamespace(name string) string { - return fmt.Sprintf("%s-%s", ProjectName, name) -} + NginxNamespace string = "ingress-nginx" -func GiteaResourceName() string { - return giteaResourceName -} + SelfSignedCertSecretName = "idpbuilder-cert" + SelfSignedCertCMName = "idpbuilder-cert" + SelfSignedCertCMKeyName = "ca.crt" + DefaultSANWildcard = "*.cnoe.localtest.me" + DefaultHostName = "cnoe.localtest.me" +) -func GitServerResourcename() string { - return gitServerResourceName +func GetProjectNamespace(name string) string { + return fmt.Sprintf("%s-%s", ProjectName, name) } diff --git a/hack/argo-cd/argocd-tls-certs-cm.yaml.tmpl b/hack/argo-cd/argocd-tls-certs-cm.yaml.tmpl new file mode 100644 index 00000000..9ed3b4f6 --- /dev/null +++ b/hack/argo-cd/argocd-tls-certs-cm.yaml.tmpl @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-tls-certs-cm + labels: + app.kubernetes.io/name: argocd-tls-certs-cm + app.kubernetes.io/part-of: argocd +data: + 'gitea.cnoe.localtest.me': | + {{ .SelfSignedCert | indentNewLines 4 }} + '{{.Host}}': | + {{ .SelfSignedCert | indentNewLines 4 }} diff --git a/hack/argo-cd/kustomization.yaml b/hack/argo-cd/kustomization.yaml index f148718b..25872b10 100644 --- a/hack/argo-cd/kustomization.yaml +++ b/hack/argo-cd/kustomization.yaml @@ -12,3 +12,4 @@ patches: - path: argocd-applicationset-controller.yaml - path: argocd-repo-server.yaml - path: argocd-redis.yaml + - path: argocd-tls-certs-cm.yaml.tmpl diff --git a/hack/ingress-nginx/deployment-ingress-nginx.yaml b/hack/ingress-nginx/deployment-ingress-nginx.yaml index 6bf69949..ff8a012b 100644 --- a/hack/ingress-nginx/deployment-ingress-nginx.yaml +++ b/hack/ingress-nginx/deployment-ingress-nginx.yaml @@ -32,6 +32,7 @@ spec: - --watch-ingress-without-class=true - --publish-status-address=localhost - --enable-ssl-passthrough + - --default-ssl-certificate=ingress-nginx/idpbuilder-cert ports: - containerPort: 80 hostPort: 80 diff --git a/pkg/build/build.go b/pkg/build/build.go index 2bda6656..3bf0564c 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -178,6 +178,13 @@ func (b *Build) Run(ctx context.Context, recreateCluster bool) error { return err } + setupLog.Info("Setting up TLS certificate") + cert, err := setupSelfSignedCertificate(ctx, setupLog, kubeClient, b.cfg) + if err != nil { + return err + } + b.cfg.SelfSignedCert = string(cert) + setupLog.V(1).Info("Running controllers") if err := b.RunControllers(ctx, mgr, managerExit, dir); err != nil { setupLog.Error(err, "Error running controllers") diff --git a/pkg/build/tls.go b/pkg/build/tls.go new file mode 100644 index 00000000..84751023 --- /dev/null +++ b/pkg/build/tls.go @@ -0,0 +1,204 @@ +package build + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io" + "math/big" + "time" + + "github.com/cnoe-io/idpbuilder/globals" + "github.com/cnoe-io/idpbuilder/pkg/k8s" + "github.com/cnoe-io/idpbuilder/pkg/util" + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + certificateOrgName = "cnoe.io" +) + +var ( + certificateValidLength = time.Hour * 8766 // one year +) + +func createIngressCertificateSecret(ctx context.Context, kubeClient client.Client, cert []byte) error { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: globals.SelfSignedCertCMName, + Namespace: corev1.NamespaceDefault, + }, + Data: map[string][]byte{ + globals.SelfSignedCertCMKeyName: cert, + }, + } + err := kubeClient.Create(ctx, secret) + if err != nil { + if k8serrors.IsAlreadyExists(err) { + return nil + } + return fmt.Errorf("creating configmap for certificate: %w", err) + } + return nil +} + +func getIngressCertificateAndKey(ctx context.Context, kubeClient client.Client, name, namespace string) ([]byte, []byte, error) { + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Type: corev1.SecretTypeTLS, + } + + err := kubeClient.Get(ctx, client.ObjectKeyFromObject(secret), secret) + if err != nil { + return nil, nil, err + } + cert, ok := secret.Data[corev1.TLSCertKey] + if !ok { + return nil, nil, fmt.Errorf("key %s not found in secret %s", corev1.TLSCertKey, name) + } + privateKey, ok := secret.Data[corev1.TLSPrivateKeyKey] + if !ok { + return nil, nil, fmt.Errorf("key %s not found in secret %s", corev1.TLSPrivateKeyKey, name) + } + + return cert, privateKey, nil +} + +func getOrCreateIngressCertificateAndKey(ctx context.Context, kubeClient client.Client, name, namespace string, sans []string) ([]byte, []byte, error) { + c, p, err := getIngressCertificateAndKey(ctx, kubeClient, name, namespace) + if err != nil { + if k8serrors.IsNotFound(err) { + cert, privateKey, cErr := createSelfSignedCertificate(sans) + if cErr != nil { + return nil, nil, cErr + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Type: corev1.SecretTypeTLS, + StringData: map[string]string{ + corev1.TLSPrivateKeyKey: string(privateKey), + corev1.TLSCertKey: string(cert), + }, + } + cErr = kubeClient.Create(ctx, secret) + if cErr != nil { + return nil, nil, fmt.Errorf("creating secret %s: %w", secret.Name, err) + } + return cert, privateKey, nil + } else { + return nil, nil, fmt.Errorf("getting secret %s: %w", name, err) + } + } + return c, p, nil +} + +func createSelfSignedCertificate(sans []string) ([]byte, []byte, error) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("generating private key: %w", err) + } + + keyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign + notBefore := time.Now() + notAfter := notBefore.Add(certificateValidLength) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, nil, fmt.Errorf("generating certificate serial number: %w", err) + } + + cert := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{certificateOrgName}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: keyUsage, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + DNSNames: sans, + } + + certBytes, err := x509.CreateCertificate(rand.Reader, &cert, &cert, &privateKey.PublicKey, privateKey) + if err != nil { + return nil, nil, fmt.Errorf("creating certificate: %w", err) + } + + var certB bytes.Buffer + var keyB bytes.Buffer + err = pem.Encode(io.Writer(&certB), &pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) + if err != nil { + return nil, nil, fmt.Errorf("encoding cert: %w", err) + } + + certOut, err := io.ReadAll(&certB) + if err != nil { + return nil, nil, fmt.Errorf("reading buffer: %w", err) + } + + privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return nil, nil, fmt.Errorf("marshal private key: %w", err) + } + + err = pem.Encode(io.Writer(&keyB), &pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyBytes}) + if err != nil { + return nil, nil, fmt.Errorf("encoding private key: %w", err) + } + privateKeyOut, err := io.ReadAll(&keyB) + if err != nil { + return nil, nil, fmt.Errorf("reading buffer: %w", err) + } + + return certOut, privateKeyOut, nil +} + +func setupSelfSignedCertificate(ctx context.Context, logger logr.Logger, kubeclient client.Client, config util.CorePackageTemplateConfig) ([]byte, error) { + if err := k8s.EnsureNamespace(ctx, kubeclient, globals.NginxNamespace); err != nil { + return nil, err + } + + sans := []string{ + globals.DefaultHostName, + globals.DefaultSANWildcard, + } + if config.Host != globals.DefaultHostName { + sans = []string{ + config.Host, + fmt.Sprintf("*.%s", config.Host), + } + } + + logger.V(1).Info("Creating/getting certificate", "host", config.Host, "sans", sans) + cert, _, err := getOrCreateIngressCertificateAndKey(ctx, kubeclient, globals.SelfSignedCertSecretName, globals.NginxNamespace, sans) + if err != nil { + return nil, err + } + + logger.V(1).Info("Creating secret for certificate", "host", config.Host) + err = createIngressCertificateSecret(ctx, kubeclient, cert) + if err != nil { + return nil, err + } + return cert, nil +} diff --git a/pkg/build/tls_test.go b/pkg/build/tls_test.go new file mode 100644 index 00000000..d6dd27c1 --- /dev/null +++ b/pkg/build/tls_test.go @@ -0,0 +1,88 @@ +package build + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "testing" + + "github.com/cnoe-io/idpbuilder/globals" + "github.com/stretchr/testify/mock" + "gotest.tools/v3/assert" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type fakeKubeClient struct { + mock.Mock + client.Client +} + +func (f *fakeKubeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + args := f.Called(ctx, key, obj, opts) + return args.Error(0) +} + +func (f *fakeKubeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + args := f.Called(ctx, obj, opts) + return args.Error(0) +} + +func TestCreateSelfSignedCertificate(t *testing.T) { + sans := []string{"cnoe.io", "*.cnoe.io"} + c, k, err := createSelfSignedCertificate(sans) + assert.NilError(t, err) + _, err = tls.X509KeyPair(c, k) + assert.NilError(t, err) + + block, _ := pem.Decode(c) + assert.Equal(t, "CERTIFICATE", block.Type) + cert, err := x509.ParseCertificate(block.Bytes) + assert.NilError(t, err) + + assert.Equal(t, 2, len(cert.DNSNames)) + expected := map[string]struct{}{ + "cnoe.io": {}, + "*.cnoe.io": {}, + } + + for _, s := range cert.DNSNames { + _, ok := expected[s] + if ok { + delete(expected, s) + } else { + t.Fatalf("unexpected key %s found", s) + } + } + assert.Equal(t, 0, len(expected)) +} + +func TestGetOrCreateIngressCertificateAndKey(t *testing.T) { + ctx := context.Background() + fClient := new(fakeKubeClient) + fClient.On("Get", ctx, client.ObjectKey{Name: globals.SelfSignedCertSecretName, Namespace: globals.NginxNamespace}, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + arg := args.Get(2).(*corev1.Secret) + d := map[string][]byte{ + corev1.TLSPrivateKeyKey: []byte("abc"), + corev1.TLSCertKey: []byte("abc"), + } + arg.Data = d + }).Return(nil) + + _, _, err := getOrCreateIngressCertificateAndKey(ctx, fClient, globals.SelfSignedCertSecretName, globals.NginxNamespace, []string{globals.DefaultHostName, globals.DefaultSANWildcard}) + assert.NilError(t, err) + fClient.AssertExpectations(t) + + fClient = new(fakeKubeClient) + fClient.On("Get", ctx, client.ObjectKey{Name: globals.SelfSignedCertSecretName, Namespace: globals.NginxNamespace}, mock.Anything, mock.Anything). + Return(k8serrors.NewNotFound(schema.GroupResource{}, "name")) + fClient.On("Create", ctx, mock.Anything, mock.Anything).Return(nil) + + c, k, err := getOrCreateIngressCertificateAndKey(ctx, fClient, globals.SelfSignedCertSecretName, globals.NginxNamespace, []string{globals.DefaultHostName, globals.DefaultSANWildcard}) + assert.NilError(t, err) + _, err = tls.X509KeyPair(c, k) + assert.NilError(t, err) +} diff --git a/pkg/cmd/create/root.go b/pkg/cmd/create/root.go index 191daf81..f33723bf 100644 --- a/pkg/cmd/create/root.go +++ b/pkg/cmd/create/root.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/cnoe-io/idpbuilder/api/v1alpha1" + "github.com/cnoe-io/idpbuilder/globals" "github.com/cnoe-io/idpbuilder/pkg/build" "github.com/cnoe-io/idpbuilder/pkg/cmd/helpers" "github.com/cnoe-io/idpbuilder/pkg/k8s" @@ -53,7 +54,7 @@ func init() { CreateCmd.PersistentFlags().StringVar(&kindConfigPath, "kind-config", "", "Path of the kind config file to be used instead of the default.") // in-cluster resources related flags - CreateCmd.PersistentFlags().StringVar(&host, "host", "cnoe.localtest.me", "Host name to access resources in this cluster.") + CreateCmd.PersistentFlags().StringVar(&host, "host", globals.DefaultHostName, "Host name to access resources in this cluster.") CreateCmd.PersistentFlags().StringVar(&ingressHost, "ingress-host-name", "", "Host name used by ingresses. Useful when you have another proxy in front of ingress-nginx that idpbuilder provisions.") CreateCmd.PersistentFlags().StringVar(&protocol, "protocol", "https", "Protocol to use to access web UIs. http or https.") CreateCmd.PersistentFlags().StringVar(&port, "port", "8443", "Port number under which idpBuilder tools are accessible.") diff --git a/pkg/controllers/gitrepository/controller_test.go b/pkg/controllers/gitrepository/controller_test.go index 41f98410..b5a009f1 100644 --- a/pkg/controllers/gitrepository/controller_test.go +++ b/pkg/controllers/gitrepository/controller_test.go @@ -20,6 +20,7 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/object" + corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -79,8 +80,10 @@ type fakeClient struct { func (f *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { s := obj.(*v1.Secret) s.Data = map[string][]byte{ - giteaAdminUsernameKey: []byte("abc"), - giteaAdminPasswordKey: []byte("abc"), + giteaAdminUsernameKey: []byte("abc"), + giteaAdminPasswordKey: []byte("abc"), + corev1.TLSCertKey: []byte("abc"), + corev1.TLSPrivateKeyKey: []byte("abc"), } return nil } diff --git a/pkg/controllers/localbuild/controller.go b/pkg/controllers/localbuild/controller.go index 2887fa46..21ca6655 100644 --- a/pkg/controllers/localbuild/controller.go +++ b/pkg/controllers/localbuild/controller.go @@ -10,18 +10,17 @@ import ( "time" argocdapp "github.com/cnoe-io/argocd-api/api/argo/application" - "github.com/cnoe-io/idpbuilder/pkg/util" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - argov1alpha1 "github.com/cnoe-io/argocd-api/api/argo/application/v1alpha1" "github.com/cnoe-io/idpbuilder/api/v1alpha1" "github.com/cnoe-io/idpbuilder/globals" "github.com/cnoe-io/idpbuilder/pkg/resources/localbuild" + "github.com/cnoe-io/idpbuilder/pkg/util" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -31,11 +30,8 @@ import ( const ( defaultArgoCDProjectName string = "default" -) - -var ( - defaultRequeueTime = time.Second * 15 - errRequeueTime = time.Second * 5 + defaultRequeueTime = time.Second * 15 + errRequeueTime = time.Second * 5 ) type LocalbuildReconciler struct { @@ -116,7 +112,7 @@ func (r *LocalbuildReconciler) installCorePackages(ctx context.Context, req ctrl defer wg.Done() _, iErr := inst(ctx, req, resource) if iErr != nil { - logger.V(1).Info("failed installing %s: %s", name, iErr) + logger.V(1).Info("failed installing", "name", name, "error", iErr) errChan <- fmt.Errorf("failed installing %s: %w", name, iErr) } }() diff --git a/pkg/controllers/localbuild/installer.go b/pkg/controllers/localbuild/installer.go index b5ca402b..8a3122b8 100644 --- a/pkg/controllers/localbuild/installer.go +++ b/pkg/controllers/localbuild/installer.go @@ -62,13 +62,8 @@ func (e *EmbeddedInstallation) Install(ctx context.Context, resource *v1alpha1.L 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 - } + if err = k8s.EnsureNamespace(ctx, nsClient, e.namespace); err != nil { + return ctrl.Result{}, err } for i := range e.unmanagedResources { diff --git a/pkg/controllers/localbuild/nginx.go b/pkg/controllers/localbuild/nginx.go index 724ecbc8..03c4bdda 100644 --- a/pkg/controllers/localbuild/nginx.go +++ b/pkg/controllers/localbuild/nginx.go @@ -5,16 +5,13 @@ import ( "embed" "github.com/cnoe-io/idpbuilder/api/v1alpha1" + "github.com/cnoe-io/idpbuilder/globals" "github.com/cnoe-io/idpbuilder/pkg/k8s" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" ) -const ( - nginxNamespace string = "ingress-nginx" -) - //go:embed resources/nginx/k8s/* var installNginxFS embed.FS @@ -27,7 +24,7 @@ func (r *LocalbuildReconciler) ReconcileNginx(ctx context.Context, req ctrl.Requ name: "Nginx", resourcePath: "resources/nginx/k8s", resourceFS: installNginxFS, - namespace: nginxNamespace, + namespace: globals.NginxNamespace, monitoredResources: map[string]schema.GroupVersionKind{ "ingress-nginx-controller": { Group: "apps", diff --git a/pkg/controllers/localbuild/resources/argo/install.yaml b/pkg/controllers/localbuild/resources/argo/install.yaml index 1eb6a2f2..187ae3f3 100644 --- a/pkg/controllers/localbuild/resources/argo/install.yaml +++ b/pkg/controllers/localbuild/resources/argo/install.yaml @@ -21154,6 +21154,11 @@ metadata: name: argocd-ssh-known-hosts-cm --- apiVersion: v1 +data: + '{{.Host}}': | + {{ .SelfSignedCert | indentNewLines 4 }} + gitea.cnoe.localtest.me: | + {{ .SelfSignedCert | indentNewLines 4 }} kind: ConfigMap metadata: labels: diff --git a/pkg/controllers/localbuild/resources/nginx/k8s/ingress-nginx.yaml b/pkg/controllers/localbuild/resources/nginx/k8s/ingress-nginx.yaml index 50914b93..8c94540d 100644 --- a/pkg/controllers/localbuild/resources/nginx/k8s/ingress-nginx.yaml +++ b/pkg/controllers/localbuild/resources/nginx/k8s/ingress-nginx.yaml @@ -404,6 +404,7 @@ spec: - --watch-ingress-without-class=true - --publish-status-address=localhost - --enable-ssl-passthrough + - --default-ssl-certificate=ingress-nginx/idpbuilder-cert env: - name: POD_NAME valueFrom: diff --git a/pkg/k8s/client.go b/pkg/k8s/client.go index 5e28dff8..b033887f 100644 --- a/pkg/k8s/client.go +++ b/pkg/k8s/client.go @@ -2,7 +2,11 @@ package k8s import ( "context" + "fmt" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -41,3 +45,21 @@ func EnsureObject(ctx context.Context, kubeClient client.Client, obj client.Obje obj.GetObjectKind().SetGroupVersionKind(curObj.GroupVersionKind()) return nil } + +func EnsureNamespace(ctx context.Context, kubeClient client.Client, name string) error { + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + + err := kubeClient.Get(ctx, client.ObjectKeyFromObject(ns), ns) + if err != nil { + if k8serrors.IsNotFound(err) { + return kubeClient.Create(ctx, ns) + } else { + return fmt.Errorf("getting namespace %s: %w", name, err) + } + } + return nil +} diff --git a/pkg/k8s/deserialize.go b/pkg/k8s/deserialize.go index e02e485f..f5de0c78 100644 --- a/pkg/k8s/deserialize.go +++ b/pkg/k8s/deserialize.go @@ -24,7 +24,7 @@ func ConvertYamlToObjects(scheme *runtime.Scheme, objYamls []byte) ([]client.Obj var k8sObjects []client.Object - for _, objYaml := range bytes.Split(objYamls, []byte{'-', '-', '-'}) { + for _, objYaml := range bytes.Split(objYamls, []byte{'\n', '-', '-', '-', '\n'}) { if len(objYaml) == 0 { continue } diff --git a/pkg/util/build_config.go b/pkg/util/build_config.go index 42b17894..9300bd64 100644 --- a/pkg/util/build_config.go +++ b/pkg/util/build_config.go @@ -6,4 +6,5 @@ type CorePackageTemplateConfig struct { IngressHost string Port string UsePathRouting bool + SelfSignedCert string } diff --git a/pkg/util/files.go b/pkg/util/files.go index 5a5cdf5c..5514a978 100644 --- a/pkg/util/files.go +++ b/pkg/util/files.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + "strings" "text/template" ) @@ -94,7 +95,10 @@ func CreateIfNotExists(dir string, perm os.FileMode) error { } func ApplyTemplate(in []byte, templateData any) ([]byte, error) { - t, err := template.New("template").Parse(string(in)) + funcMap := template.FuncMap{ + "indentNewLines": templateIndentNewlines, + } + t, err := template.New("template").Funcs(funcMap).Parse(string(in)) if err != nil { return nil, err } @@ -108,3 +112,8 @@ func ApplyTemplate(in []byte, templateData any) ([]byte, error) { return ret.Bytes(), nil } + +// indent given string with given number of spaces whenever a newline symbol is found. +func templateIndentNewlines(n int, val string) string { + return strings.Replace(val, "\n", "\n"+strings.Repeat(" ", n), -1) +}