From ea80734f917a61d4c6e7e0f6b43412f6558b593c Mon Sep 17 00:00:00 2001 From: AhmedGrati <48932084+AhmedGrati@users.noreply.github.com> Date: Thu, 24 Aug 2023 11:38:21 +0100 Subject: [PATCH] Feat: add kompose client PoC (#1593) * fix: support host port and protocol in functional tests * feat: add kompose client with options Signed-off-by: AhmedGrati * test: add options unit tests Signed-off-by: AhmedGrati * feat: add partial convert options Signed-off-by: AhmedGrati * feat: finish convert process Signed-off-by: AhmedGrati * test: finish unit tests of the kompose client Signed-off-by: AhmedGrati * remove unecessary changes Signed-off-by: AhmedGrati * feat: add generate network policies to client Signed-off-by: AhmedGrati * update go mod Signed-off-by: AhmedGrati --------- Signed-off-by: AhmedGrati --- .gitignore | 3 + Makefile | 4 + client/client.go | 21 +++ client/convert.go | 248 ++++++++++++++++++++++++++++ client/convert_test.go | 83 ++++++++++ client/options.go | 25 +++ client/options_test.go | 61 +++++++ client/testdata/docker-compose.yaml | 7 + client/types.go | 69 ++++++++ go.mod | 1 + go.sum | 1 + pkg/app/app.go | 10 +- 12 files changed, 530 insertions(+), 3 deletions(-) create mode 100644 client/client.go create mode 100644 client/convert.go create mode 100644 client/convert_test.go create mode 100644 client/options.go create mode 100644 client/options_test.go create mode 100644 client/testdata/docker-compose.yaml create mode 100644 client/types.go diff --git a/.gitignore b/.gitignore index 9d334104..d930ba43 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,6 @@ tags .idea .DS_Store + +# Client Test generated files +client/testdata/generated diff --git a/Makefile b/Makefile index 34a1b61a..09ae0531 100644 --- a/Makefile +++ b/Makefile @@ -138,3 +138,7 @@ install-golangci-lint: .PHONY: golangci-lint golangci-lint: install-golangci-lint $(GOLANGCI_LINT) run -c .golangci.yml --timeout 5m + +.PHONY: test-client +test-client: + go test ./client/... diff --git a/client/client.go b/client/client.go new file mode 100644 index 00000000..59536682 --- /dev/null +++ b/client/client.go @@ -0,0 +1,21 @@ +package client + +type Kompose struct { + suppressWarnings bool + verbose bool + errorOnWarning bool +} + +func NewClient(opts ...Opt) (*Kompose, error) { + k := &Kompose{ + suppressWarnings: false, + verbose: false, + errorOnWarning: false, + } + for _, op := range opts { + if err := op(k); err != nil { + return nil, err + } + } + return k, nil +} diff --git a/client/convert.go b/client/convert.go new file mode 100644 index 00000000..3e62a622 --- /dev/null +++ b/client/convert.go @@ -0,0 +1,248 @@ +package client + +import ( + "fmt" + + "github.com/kubernetes/kompose/pkg/app" + "github.com/kubernetes/kompose/pkg/kobject" + "k8s.io/apimachinery/pkg/runtime" +) + +func (k *Kompose) Convert(options ConvertOptions) ([]runtime.Object, error) { + options = k.setDefaultValues(options) + err := k.validateOptions(options) + if err != nil { + return nil, err + } + kobjectConvertOptions := kobject.ConvertOptions{ + ToStdout: options.ToStdout, + CreateChart: k.createChart(options), + GenerateYaml: true, + GenerateJSON: options.GenerateJson, + Replicas: *options.Replicas, + InputFiles: options.InputFiles, + OutFile: options.OutFile, + Provider: k.getProvider(options), + CreateD: k.createDeployment(options), + CreateDS: k.createDaemonSet(options), + CreateRC: k.createReplicationController(options), + Build: *options.Build, + BuildRepo: k.buildRepo(options), + BuildBranch: k.buildBranch(options), + PushImage: options.PushImage, + PushImageRegistry: options.PushImageRegistry, + CreateDeploymentConfig: k.createDeploymentConfig(options), + EmptyVols: false, + Volumes: *options.VolumeType, + PVCRequestSize: options.PvcRequestSize, + InsecureRepository: k.insecureRepository(options), + IsDeploymentFlag: k.createDeployment(options), + IsDaemonSetFlag: k.createDaemonSet(options), + IsReplicationControllerFlag: k.createReplicationController(options), + Controller: k.getController(options), + IsReplicaSetFlag: *options.Replicas != 0, + IsDeploymentConfigFlag: k.createDeploymentConfig(options), + YAMLIndent: 2, + WithKomposeAnnotation: *options.WithKomposeAnnotations, + MultipleContainerMode: k.multiContainerMode(options), + ServiceGroupMode: k.serviceGroupMode(options), + ServiceGroupName: k.serviceGroupName(options), + SecretsAsFiles: k.secretsAsFiles(options), + GenerateNetworkPolicies: options.GenerateNetworkPolicies, + } + err = app.ValidateComposeFile(&kobjectConvertOptions) + if err != nil { + return nil, err + } + objects, err := app.Convert(kobjectConvertOptions) + return objects, err +} + +func (k *Kompose) setDefaultValues(options ConvertOptions) ConvertOptions { + replicasDefaultValue := 1 + buildDefaultValue := "none" + volumeTypeDefaultValue := "persistentVolumeClaim" + withKomposeAnnotationsDefaultValue := true + kubernetesControllerDefaultValue := "deployment" + kubernetesServiceGroupModeDefaultValue := "" + + if options.Replicas == nil { + options.Replicas = &replicasDefaultValue + } + if options.Build == nil { + options.Build = &buildDefaultValue + } + if options.VolumeType == nil { + options.VolumeType = &volumeTypeDefaultValue + } + if options.WithKomposeAnnotations == nil { + options.WithKomposeAnnotations = &withKomposeAnnotationsDefaultValue + } + if options.Provider == nil { + options.Provider = Kubernetes{ + Controller: &kubernetesControllerDefaultValue, + } + } + if kubernetesProvider, ok := options.Provider.(Kubernetes); ok { + if kubernetesProvider.Controller == nil { + options.Provider = Kubernetes{ + Controller: &kubernetesControllerDefaultValue, + Chart: options.Provider.(Kubernetes).Chart, + MultiContainerMode: options.Provider.(Kubernetes).MultiContainerMode, + ServiceGroupMode: options.Provider.(Kubernetes).ServiceGroupMode, + ServiceGroupName: options.Provider.(Kubernetes).ServiceGroupName, + SecretsAsFiles: options.Provider.(Kubernetes).SecretsAsFiles, + } + } + if kubernetesProvider.ServiceGroupMode == nil { + options.Provider = Kubernetes{ + Controller: options.Provider.(Kubernetes).Controller, + Chart: options.Provider.(Kubernetes).Chart, + MultiContainerMode: options.Provider.(Kubernetes).MultiContainerMode, + ServiceGroupMode: &kubernetesServiceGroupModeDefaultValue, + ServiceGroupName: options.Provider.(Kubernetes).ServiceGroupName, + SecretsAsFiles: options.Provider.(Kubernetes).SecretsAsFiles, + } + } + } + return options +} + +func (k *Kompose) validateOptions(options ConvertOptions) error { + build := options.Build + if *build != string(LOCAL) && *build != string(BUILD_CONFIG) && *build != string(NONE) { + return fmt.Errorf( + "unexpected Value for Build field. Possible values are: %v, %v, and %v", string(LOCAL), string(BUILD_CONFIG), string(NONE), + ) + } + + volumeType := options.VolumeType + if *volumeType != string(PVC) && *volumeType != string(EMPTYDIR) && *volumeType != string(HOSTPATH) && *volumeType != string(CONFIGMAP) { + return fmt.Errorf( + "unexpected Value for VolumeType field. Possible values are: %v, %v, %v, %v", string(PVC), string(EMPTYDIR), string(HOSTPATH), string(CONFIGMAP), + ) + } + + if kubernetesProvider, ok := options.Provider.(Kubernetes); ok { + kubernetesController := kubernetesProvider.Controller + if *kubernetesController != string(DEPLOYMENT) && *kubernetesController != string(DAEMONSET) && *kubernetesController != string(REPLICATION_CONTROLLER) { + return fmt.Errorf( + "unexpected Value for Kubernetes Controller field. Possible values are: %v, %v, and %v", string(DEPLOYMENT), string(DAEMONSET), string(REPLICATION_CONTROLLER), + ) + } + + kubernetesServiceGroupMode := kubernetesProvider.ServiceGroupMode + if *kubernetesServiceGroupMode != string(LABEL) && *kubernetesServiceGroupMode != string(VOLUME) && *kubernetesServiceGroupMode != "" { + return fmt.Errorf( + "unexpected Value for Kubernetes Service Groupe Mode field. Possible values are: %v, %v, ''", string(LABEL), string(VOLUME), + ) + } + + if *build == string(BUILD_CONFIG) { + return fmt.Errorf("the build value %v is only supported for Openshift provider", string(BUILD_CONFIG)) + } + } + + return nil +} + +func (k *Kompose) createDeployment(options ConvertOptions) bool { + if kubernetesProvider, ok := options.Provider.(Kubernetes); ok { + return *kubernetesProvider.Controller == string(DEPLOYMENT) + } + return false +} + +func (k *Kompose) createDaemonSet(options ConvertOptions) bool { + if kubernetesProvider, ok := options.Provider.(Kubernetes); ok { + return *kubernetesProvider.Controller == string(DAEMONSET) + } + return false +} + +func (k *Kompose) createReplicationController(options ConvertOptions) bool { + if kubernetesProvider, ok := options.Provider.(Kubernetes); ok { + return *kubernetesProvider.Controller == string(REPLICATION_CONTROLLER) + } + return false +} + +func (k *Kompose) createChart(options ConvertOptions) bool { + if kubernetesProvider, ok := options.Provider.(Kubernetes); ok { + return kubernetesProvider.Chart + } + return false +} + +func (k *Kompose) multiContainerMode(options ConvertOptions) bool { + if kubernetesProvider, ok := options.Provider.(Kubernetes); ok { + return kubernetesProvider.MultiContainerMode + } + return false +} + +func (k *Kompose) serviceGroupMode(options ConvertOptions) string { + if kubernetesProvider, ok := options.Provider.(Kubernetes); ok { + return *kubernetesProvider.ServiceGroupMode + } + return "" +} + +func (k *Kompose) serviceGroupName(options ConvertOptions) string { + if kubernetesProvider, ok := options.Provider.(Kubernetes); ok { + return kubernetesProvider.ServiceGroupName + } + return "" +} + +func (k *Kompose) secretsAsFiles(options ConvertOptions) bool { + if kubernetesProvider, ok := options.Provider.(Kubernetes); ok { + return kubernetesProvider.SecretsAsFiles + } + return false +} + +func (k *Kompose) createDeploymentConfig(options ConvertOptions) bool { + if _, ok := options.Provider.(Openshift); ok { + return true + } + return false +} + +func (k *Kompose) insecureRepository(options ConvertOptions) bool { + if openshiftProvider, ok := options.Provider.(Openshift); ok { + return openshiftProvider.InsecureRepository + } + return false +} + +func (k *Kompose) buildRepo(options ConvertOptions) string { + if openshiftProvider, ok := options.Provider.(Openshift); ok { + return openshiftProvider.BuildRepo + } + return "" +} + +func (k *Kompose) buildBranch(options ConvertOptions) string { + if openshiftProvider, ok := options.Provider.(Openshift); ok { + return openshiftProvider.BuildRepo + } + return "" +} + +func (k *Kompose) getProvider(options ConvertOptions) string { + if _, ok := options.Provider.(Openshift); ok { + return "openshift" + } + if _, ok := options.Provider.(Kubernetes); ok { + return "kubernetes" + } + return "kubernetes" +} + +func (k *Kompose) getController(options ConvertOptions) string { + if kubernetesProvider, ok := options.Provider.(Kubernetes); ok { + return *kubernetesProvider.Controller + } + return "" +} diff --git a/client/convert_test.go b/client/convert_test.go new file mode 100644 index 00000000..b7014936 --- /dev/null +++ b/client/convert_test.go @@ -0,0 +1,83 @@ +package client + +import ( + "fmt" + "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" + appsv1 "k8s.io/api/apps/v1" +) + +func TestConvertError(t *testing.T) { + randomBuildValue := "random-build" + randomVolumeTypeValue := "random-volume-type" + randomKubernetesControllerValue := "random-controller" + randomKubernetesServiceGroupModeValue := "random-group-mode" + buildConfigValue := string(BUILD_CONFIG) + testCases := []struct { + options ConvertOptions + errorMessage string + }{ + { + options: ConvertOptions{ + Build: &randomBuildValue, + }, + errorMessage: fmt.Sprintf("unexpected Value for Build field. Possible values are: %v, %v, and %v", string(LOCAL), string(BUILD_CONFIG), string(NONE)), + }, + { + options: ConvertOptions{ + VolumeType: &randomVolumeTypeValue, + }, + errorMessage: fmt.Sprintf("unexpected Value for VolumeType field. Possible values are: %v, %v, %v, %v", string(PVC), string(EMPTYDIR), string(HOSTPATH), string(CONFIGMAP)), + }, + { + options: ConvertOptions{ + Provider: Kubernetes{ + Controller: &randomKubernetesControllerValue, + }, + }, + errorMessage: fmt.Sprintf("unexpected Value for Kubernetes Controller field. Possible values are: %v, %v, and %v", string(DEPLOYMENT), string(DAEMONSET), string(REPLICATION_CONTROLLER)), + }, + { + options: ConvertOptions{ + Provider: Kubernetes{ + ServiceGroupMode: &randomKubernetesServiceGroupModeValue, + }, + }, + errorMessage: fmt.Sprintf("unexpected Value for Kubernetes Service Groupe Mode field. Possible values are: %v, %v, ''", string(LABEL), string(VOLUME)), + }, + { + options: ConvertOptions{ + Provider: Kubernetes{}, + Build: &buildConfigValue, + }, + errorMessage: fmt.Sprintf("the build value %v is only supported for Openshift provider", string(BUILD_CONFIG)), + }, + } + + client, err := NewClient() + assert.Check(t, is.Equal(err, nil)) + for _, tc := range testCases { + _, err := client.Convert(tc.options) + + assert.Check(t, is.Equal(err.Error(), tc.errorMessage)) + } +} + +func TestConvertWithDefaultOptions(t *testing.T) { + client, err := NewClient(WithErrorOnWarning()) + assert.Check(t, is.Equal(err, nil)) + objects, err := client.Convert(ConvertOptions{ + OutFile: "./testdata/generated/", + InputFiles: []string{ + "./testdata/docker-compose.yaml", + }, + }) + assert.Check(t, is.Equal(err, nil)) + for _, object := range objects { + if deployment, ok := object.(*appsv1.Deployment); ok { + assert.Check(t, is.Equal(int(*deployment.Spec.Replicas), 1)) + } + } +} diff --git a/client/options.go b/client/options.go new file mode 100644 index 00000000..7d9d4098 --- /dev/null +++ b/client/options.go @@ -0,0 +1,25 @@ +package client + +// Opt is a configuration option to initialize a client +type Opt func(*Kompose) error + +func WithSuppressWarnings() Opt { + return func(k *Kompose) error { + k.suppressWarnings = true + return nil + } +} + +func WithVerboseOutput() Opt { + return func(k *Kompose) error { + k.verbose = true + return nil + } +} + +func WithErrorOnWarning() Opt { + return func(k *Kompose) error { + k.errorOnWarning = true + return nil + } +} diff --git a/client/options_test.go b/client/options_test.go new file mode 100644 index 00000000..d5e51cac --- /dev/null +++ b/client/options_test.go @@ -0,0 +1,61 @@ +package client + +import ( + "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestNewClientWithOpts(t *testing.T) { + testCases := []struct { + expectedError error + expectedSuppressWarnings bool + expectedVerbose bool + expectedErrorOnWarnings bool + opts []Opt + }{ + { + expectedError: nil, + expectedSuppressWarnings: false, + expectedVerbose: false, + expectedErrorOnWarnings: false, + opts: []Opt{}, + }, + { + expectedError: nil, + expectedSuppressWarnings: true, + expectedVerbose: false, + expectedErrorOnWarnings: false, + opts: []Opt{WithSuppressWarnings()}, + }, + { + expectedError: nil, + expectedSuppressWarnings: false, + expectedVerbose: true, + expectedErrorOnWarnings: false, + opts: []Opt{WithVerboseOutput()}, + }, + { + expectedError: nil, + expectedSuppressWarnings: false, + expectedVerbose: false, + expectedErrorOnWarnings: true, + opts: []Opt{WithErrorOnWarning()}, + }, + { + expectedError: nil, + expectedSuppressWarnings: true, + expectedVerbose: false, + expectedErrorOnWarnings: true, + opts: []Opt{WithErrorOnWarning(), WithSuppressWarnings()}, + }, + } + for _, tc := range testCases { + client, err := NewClient(tc.opts...) + assert.Check(t, is.Equal(err, tc.expectedError)) + assert.Check(t, is.Equal(client.errorOnWarning, tc.expectedErrorOnWarnings)) + assert.Check(t, is.Equal(client.verbose, tc.expectedVerbose)) + assert.Check(t, is.Equal(client.suppressWarnings, tc.expectedSuppressWarnings)) + } +} diff --git a/client/testdata/docker-compose.yaml b/client/testdata/docker-compose.yaml new file mode 100644 index 00000000..a7df1309 --- /dev/null +++ b/client/testdata/docker-compose.yaml @@ -0,0 +1,7 @@ +version: '3' +services: + web: + image: nginx:latest + ports: + - "80:80" + diff --git a/client/types.go b/client/types.go new file mode 100644 index 00000000..35e94abe --- /dev/null +++ b/client/types.go @@ -0,0 +1,69 @@ +package client + +type ConvertBuild string + +const ( + LOCAL ConvertBuild = "local" + BUILD_CONFIG ConvertBuild = "build-config" + NONE ConvertBuild = "none" +) + +type KubernetesController string + +const ( + DEPLOYMENT KubernetesController = "deployment" + DAEMONSET KubernetesController = "daemonSet" + REPLICATION_CONTROLLER KubernetesController = "replicationController" +) + +type ServiceGroupMode string + +const ( + LABEL ServiceGroupMode = "label" + VOLUME ServiceGroupMode = "volume" +) + +type VolumeType string + +const ( + PVC = "persistentVolumeClaim" + EMPTYDIR = "emptyDir" + HOSTPATH = "hostPath" + CONFIGMAP = "configMap" +) + +type ConvertOptions struct { + Build *string + PushImage bool + PushImageRegistry string + GenerateJson bool + ToStdout bool + OutFile string + Replicas *int + VolumeType *string + PvcRequestSize string + WithKomposeAnnotations *bool + InputFiles []string + Provider + GenerateNetworkPolicies bool +} + +type Provider interface{} + +type Kubernetes struct { + Provider + Chart bool + Controller *string + MultiContainerMode bool + ServiceGroupMode *string + ServiceGroupName string + SecretsAsFiles bool +} + +type Openshift struct { + Provider + DeploymentConfig bool + InsecureRepository bool + BuildRepo string + BuildBranch string +} diff --git a/go.mod b/go.mod index 35061c5b..5da06632 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/spf13/viper v1.16.0 golang.org/x/tools v0.12.0 gopkg.in/yaml.v3 v3.0.1 + gotest.tools/v3 v3.4.0 k8s.io/api v0.28.0 k8s.io/apimachinery v0.28.0 ) diff --git a/go.sum b/go.sum index 43a7c7b1..0b3c6e10 100644 --- a/go.sum +++ b/go.sum @@ -615,6 +615,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/app/app.go b/pkg/app/app.go index c1752789..25a930a1 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -22,6 +22,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime" "os" @@ -143,20 +144,22 @@ func ValidateFlags(args []string, cmd *cobra.Command, opt *kobject.ConvertOption } // ValidateComposeFile validates the compose file provided for conversion -func ValidateComposeFile(opt *kobject.ConvertOptions) { +func ValidateComposeFile(opt *kobject.ConvertOptions) error { if len(opt.InputFiles) == 0 { for _, name := range DefaultComposeFiles { _, err := os.Stat(name) if err != nil { log.Debugf("'%s' not found: %v", name, err) + return err } else { opt.InputFiles = []string{name} - return + return nil } } log.Fatal("No 'docker-compose' file found") } + return nil } func validateControllers(opt *kobject.ConvertOptions) { @@ -202,7 +205,7 @@ func validateControllers(opt *kobject.ConvertOptions) { } // Convert transforms docker compose or dab file to k8s objects -func Convert(opt kobject.ConvertOptions) { +func Convert(opt kobject.ConvertOptions) ([]runtime.Object, error) { validateControllers(&opt) // loader parses input from file into komposeObject. @@ -236,6 +239,7 @@ func Convert(opt kobject.ConvertOptions) { if err != nil { log.Fatalf(err.Error()) } + return objects, err } // Convenience method to return the appropriate Transformer based on