diff --git a/assets/knative/service.yaml.tmpl b/assets/knative/service.yaml.tmpl index bc898f5..1d17d05 100644 --- a/assets/knative/service.yaml.tmpl +++ b/assets/knative/service.yaml.tmpl @@ -3,10 +3,6 @@ kind: Service metadata: name: {{ .Name }} namespace: default - labels: - {{ if .PrivateService }} - networking.knative.dev/visibility: cluster-local - {{ end }} spec: template: spec: diff --git a/src/cli/cli.go b/src/cli/cli.go index 3eb494f..3b3dd4a 100644 --- a/src/cli/cli.go +++ b/src/cli/cli.go @@ -69,7 +69,6 @@ func (c *icli) init(cCtx *cli.Context) error { registry := cCtx.String(repoNameFlag) imagePullSecrets := cCtx.StringSlice(imagePullSecretsFlag) dockerFileName := cCtx.String(dockerFileNameFlag) - isPublicService := cCtx.Bool(isPublicServiceFlag) if dockerFileName == "" { dockerFileName = defaults.GeneratedDockerFile @@ -85,7 +84,7 @@ func (c *icli) init(cCtx *cli.Context) error { projectDirectory, cCtx.String(runtimeVersionFlag), version, - isPublicService, + cCtx.Bool(isPrivateServiceFlag), imagePullSecrets, c.Resources, ) diff --git a/src/cli/deploy.go b/src/cli/deploy.go index a1afa96..4b928a9 100644 --- a/src/cli/deploy.go +++ b/src/cli/deploy.go @@ -1,40 +1,79 @@ package cli import ( + "fmt" + "github.com/nearform/initium-cli/src/services/git" knative "github.com/nearform/initium-cli/src/services/k8s" "github.com/urfave/cli/v2" ) func (c *icli) Deploy(cCtx *cli.Context) error { - config, err := knative.Config( - cCtx.String(endpointFlag), - cCtx.String(tokenFlag), - []byte(cCtx.String(caCRTFlag)), - ) + namespace := cCtx.String(namespaceFlag) + envFile := cCtx.String(envVarFileFlag) + project, err := c.getProject(cCtx) if err != nil { return err } - project, err := c.getProject(cCtx) + + commitSha, err := git.GetHash() if err != nil { return err } - commitSha, err := git.GetHash() + serviceManifest, err := knative.LoadManifest(namespace, commitSha, project, c.dockerImage, envFile) if err != nil { return err } - return knative.Apply(cCtx.String(namespaceFlag), commitSha, config, project, c.dockerImage, cCtx.String(envVarFileFlag)) + if cCtx.Bool(dryRunFlag) { + yamlBytes, err := knative.ToYaml(serviceManifest) + if err != nil { + return err + } + fmt.Fprintf(c.Writer, "%s", yamlBytes) + return nil + } + + config, err := knative.Config( + cCtx.String(endpointFlag), + cCtx.String(tokenFlag), + []byte(cCtx.String(caCRTFlag)), + ) + + if err != nil { + return err + } + + return knative.Apply(serviceManifest, config) } func (c icli) DeployCMD() *cli.Command { + flags := c.CommandFlags([]FlagsType{Kubernetes, Shared}) + + flags = append(flags, &cli.BoolFlag{ + Name: dryRunFlag, + Usage: "print out the knative manifest without applying it", + Value: false, + }) + return &cli.Command{ Name: "deploy", Usage: "deploy the application as a knative service", - Flags: c.CommandFlags([]FlagsType{Kubernetes, Shared}), + Flags: flags, Action: c.Deploy, - Before: c.baseBeforeFunc, + Before: func(ctx *cli.Context) error { + if err := c.loadFlagsFromConfig(ctx); err != nil { + return err + } + + ignoredFlags := []string{} + if ctx.Bool(dryRunFlag) { + ignoredFlags = append(ignoredFlags, endpointFlag, tokenFlag, caCRTFlag) + } + + return c.checkRequiredFlags(ctx, ignoredFlags) + }, } } diff --git a/src/cli/flags.go b/src/cli/flags.go index 8de7094..913f7e1 100644 --- a/src/cli/flags.go +++ b/src/cli/flags.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "os" + "strconv" "strings" "github.com/nearform/initium-cli/src/services/git" @@ -46,7 +47,8 @@ const ( stopOnBuildFlag string = "stop-on-build" stopOnPushFlag string = "stop-on-push" envVarFileFlag string = "env-var-file" - isPublicServiceFlag string = "public" + isPrivateServiceFlag string = "private" + dryRunFlag string = "dry-run" ) type flags struct { @@ -110,6 +112,12 @@ func InitFlags() flags { Required: false, Category: "deploy", }, + &cli.BoolFlag{ + Name: isPrivateServiceFlag, + Usage: "Do not expose the service public endpoint", + Category: "init", + Value: false, + }, &cli.StringFlag{ Name: envVarFileFlag, Value: defaults.EnvVarFile, @@ -157,12 +165,6 @@ func InitFlags() flags { Value: defaults.ConfigFile, EnvVars: []string{"INITIUM_CONFIG_FILE"}, }, - &cli.BoolFlag{ - Name: isPublicServiceFlag, - Usage: "will deploy the service as accessible from outside of the cluster", - Category: "init", - Value: false, - }, }, Shared: []cli.Flag{ &cli.StringFlag{ @@ -257,9 +259,17 @@ func (c icli) loadFlagsFromConfig(ctx *cli.Context) error { for _, name := range v.Names() { c.Logger.Debugf("%s is set = %v", name, ctx.IsSet(name)) if name != "help" && !slices.Contains(excludedFlags, name) && config[name] != nil && !ctx.IsSet(mainName) { - if err := ctx.Set(mainName, config[name].(string)); err != nil { - return err + switch config[name].(type) { + case bool: + if err := ctx.Set(mainName, strconv.FormatBool(config[name].(bool))); err != nil { + return err + } + default: + if err := ctx.Set(mainName, config[name].(string)); err != nil { + return err + } } + } } } diff --git a/src/cli/init_test.go b/src/cli/init_test.go index 1356a01..0e2e6e3 100644 --- a/src/cli/init_test.go +++ b/src/cli/init_test.go @@ -20,7 +20,7 @@ default-branch: main dockerfile-name: null env-var-file: .env.initium image-pull-secrets: null -public: %t +private: %t runtime-version: null `, appName, @@ -120,7 +120,7 @@ func TestRepoNameRetrocompatibiliy(t *testing.T) { func TestAppName(t *testing.T) { cli := GeticliForTesting(os.DirFS("../..")) - + cli.Writer = new(bytes.Buffer) err := cli.Run([]string{"initium", "build"}) if err == nil { t.Errorf("CLI should ask for %s and %s if not detected", appNameFlag, repoNameFlag) diff --git a/src/services/k8s/knative.go b/src/services/k8s/knative.go index df9305d..eb9d65d 100644 --- a/src/services/k8s/knative.go +++ b/src/services/k8s/knative.go @@ -15,10 +15,13 @@ import ( "github.com/nearform/initium-cli/src/services/docker" "github.com/nearform/initium-cli/src/services/project" "github.com/nearform/initium-cli/src/utils/defaults" + "sigs.k8s.io/yaml" corev1 "k8s.io/api/core/v1" apimachineryErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -30,6 +33,8 @@ import ( const ( UpdateShaAnnotationName = "initium.nearform.com/updateSha" UpdateTimestampAnnotationName = "initium.nearform.com/updateTimestamp" + visibilityLabel = "networking.knative.dev/visibility" + visibilityLabelPrivateValue = "cluster-local" ) func Config(endpoint string, token string, caCrt []byte) (*rest.Config, error) { @@ -46,7 +51,16 @@ func Config(endpoint string, token string, caCrt []byte) (*rest.Config, error) { }, nil } -func loadManifest(namespace string, commitSha string, project *project.Project, dockerImage docker.DockerImage, envFile string) (*servingv1.Service, error) { +func setLabels(manifest *servingv1.Service, project project.Project) { + if manifest.ObjectMeta.Labels == nil { + manifest.ObjectMeta.Labels = map[string]string{} + } + if project.IsPrivate { + manifest.ObjectMeta.Labels[visibilityLabel] = visibilityLabelPrivateValue + } +} + +func LoadManifest(namespace string, commitSha string, project *project.Project, dockerImage docker.DockerImage, envFile string) (*servingv1.Service, error) { knativeTemplate := path.Join("assets", "knative", "service.yaml.tmpl") template, err := template.ParseFS(project.Resources, knativeTemplate) if err != nil { @@ -57,7 +71,6 @@ func loadManifest(namespace string, commitSha string, project *project.Project, "Name": dockerImage.Name, "RemoteTag": dockerImage.RemoteTag(), "ImagePullSecrets": project.ImagePullSecrets, - "PrivateService": !project.IsPublicService, } output := &bytes.Buffer{} @@ -91,16 +104,24 @@ func loadManifest(namespace string, commitSha string, project *project.Project, UpdateTimestampAnnotationName: time.Now().Format(time.RFC3339), } - envVarList, err := loadEnvFile(envFile) - if err != nil { + setLabels(service, *project) + if err = setEnv(service, envFile); err != nil { return nil, err } - service.Spec.Template.Spec.Containers[0].Env = append(service.Spec.Template.Spec.Containers[0].Env, envVarList...) - return service, nil } +func setEnv(manifest *servingv1.Service, envFile string) error { + envVarList, err := loadEnvFile(envFile) + if err != nil { + return err + } + + manifest.Spec.Template.Spec.Containers[0].Env = append(manifest.Spec.Template.Spec.Containers[0].Env, envVarList...) + return nil +} + func loadEnvFile(envFile string) ([]corev1.EnvVar, error) { var envVarList []corev1.EnvVar if _, err := os.Stat(envFile); err != nil { @@ -157,15 +178,21 @@ func loadEnvFile(envFile string) ([]corev1.EnvVar, error) { return envVarList, nil } -func Apply(namespace string, commitSha string, config *rest.Config, project *project.Project, dockerImage docker.DockerImage, envFile string) error { - log.Info("Deploying Knative service", "host", config.Host, "name", project.Name, "namespace", namespace) - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) - defer cancel() - - serviceManifest, err := loadManifest(namespace, commitSha, project, dockerImage, envFile) +func ToYaml(serviceManifest *servingv1.Service) ([]byte, error) { + scheme := runtime.NewScheme() + servingv1.AddToScheme(scheme) + codec := serializer.NewCodecFactory(scheme).LegacyCodec(servingv1.SchemeGroupVersion) + jsonBytes, err := runtime.Encode(codec, serviceManifest) if err != nil { - return err + return nil, err } + return yaml.JSONToYAML(jsonBytes) +} + +func Apply(serviceManifest *servingv1.Service, config *rest.Config) error { + log.Info("Deploying Knative service", "host", config.Host, "name", serviceManifest.ObjectMeta.Name, "namespace", serviceManifest.ObjectMeta.Namespace) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer cancel() // Create a new Knative Serving client servingClient, err := servingv1client.NewForConfig(config) diff --git a/src/services/k8s/knative_test.go b/src/services/k8s/knative_test.go index c9f9546..4770435 100644 --- a/src/services/k8s/knative_test.go +++ b/src/services/k8s/knative_test.go @@ -60,9 +60,9 @@ func TestLoadManifestForPrivateService(t *testing.T) { commitSha := "93f4be93" proj := &project.Project{Name: "knative_test", - Directory: path.Join(root, "example"), - Resources: os.DirFS(root), - IsPublicService: false, + Directory: path.Join(root, "example"), + Resources: os.DirFS(root), + IsPrivate: false, } dockerImage := docker.DockerImage{ @@ -72,7 +72,7 @@ func TestLoadManifestForPrivateService(t *testing.T) { Tag: "v1.1.0", } - serviceManifest, err := loadManifest(namespace, commitSha, proj, dockerImage, path.Join(root, "example/.env.sample")) + serviceManifest, err := LoadManifest(namespace, commitSha, proj, dockerImage, path.Join(root, "example/.env.sample")) if err != nil { t.Fatalf(fmt.Sprintf("Error: %v", err)) @@ -82,8 +82,9 @@ func TestLoadManifestForPrivateService(t *testing.T) { assert.Assert(t, annotations[UpdateTimestampAnnotationName] != "", "Missing %s annotation", UpdateTimestampAnnotationName) assert.Assert(t, annotations[UpdateShaAnnotationName] == commitSha, "Expected %s SHA, got %s", commitSha, annotations[UpdateShaAnnotationName]) - labels := serviceManifest.ObjectMeta.Labels - assert.Assert(t, labels["networking.knative.dev/visibility"] == "cluster-local", "Missing networking.knative.dev/visibility label with cluster-local value") + labels := serviceManifest.GetLabels() + _, ok := labels[visibilityLabel] + assert.Assert(t, !ok, "Visibility label should not be set for public services") } func TestLoadManifestForPublicService(t *testing.T) { @@ -95,7 +96,7 @@ func TestLoadManifestForPublicService(t *testing.T) { Directory: path.Join(root, "example"), Resources: os.DirFS(root), ImagePullSecrets: imagePullSecrets, - IsPublicService: true, + IsPrivate: true, } dockerImage := docker.DockerImage{ @@ -105,7 +106,7 @@ func TestLoadManifestForPublicService(t *testing.T) { Tag: "v1.1.0", } - serviceManifest, err := loadManifest(namespace, commitSha, proj, dockerImage, path.Join(root, "example/.env.sample")) + serviceManifest, err := LoadManifest(namespace, commitSha, proj, dockerImage, path.Join(root, "example/.env.sample")) if err != nil { t.Fatalf(fmt.Sprintf("Error: %v", err)) @@ -117,6 +118,6 @@ func TestLoadManifestForPublicService(t *testing.T) { assert.Assert(t, annotations[UpdateShaAnnotationName] == commitSha, "Expected %s SHA, got %s", commitSha, annotations[UpdateShaAnnotationName]) assert.Assert(t, pullSecret == imagePullSecrets[0], "Expected secret value to be %s, got %s", imagePullSecrets, pullSecret) - labels := serviceManifest.ObjectMeta.Labels - assert.Assert(t, labels["networking.knative.dev/visibility"] == "", "Label networking.knative.dev/visibility should not be present") + labels := serviceManifest.GetLabels() + assert.Assert(t, labels[visibilityLabel] == visibilityLabelPrivateValue) } diff --git a/src/services/project/project.go b/src/services/project/project.go index bfe5041..0c486f3 100644 --- a/src/services/project/project.go +++ b/src/services/project/project.go @@ -27,7 +27,7 @@ type Project struct { DefaultRuntimeVersion string ImagePullSecrets []string Resources fs.FS - IsPublicService bool + IsPrivate bool } type InitOptions struct { @@ -45,7 +45,7 @@ func GuessAppName() *string { return &name } -func New(name string, directory string, runtimeVersion string, version string, isPublicService bool, imagePullSecrets []string, resources fs.FS) Project { +func New(name string, directory string, runtimeVersion string, version string, isPrivate bool, imagePullSecrets []string, resources fs.FS) Project { return Project{ Name: name, Directory: directory, @@ -53,7 +53,7 @@ func New(name string, directory string, runtimeVersion string, version string, i ImagePullSecrets: imagePullSecrets, Resources: resources, Version: version, - IsPublicService: isPublicService, + IsPrivate: isPrivate, } }