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 <ahmedgrati1999@gmail.com>

* test: add options unit tests

Signed-off-by: AhmedGrati <ahmedgrati1999@gmail.com>

* feat: add partial convert options

Signed-off-by: AhmedGrati <ahmedgrati1999@gmail.com>

* feat: finish convert process

Signed-off-by: AhmedGrati <ahmedgrati1999@gmail.com>

* test: finish unit tests of the kompose client

Signed-off-by: AhmedGrati <ahmedgrati1999@gmail.com>

* remove unecessary changes

Signed-off-by: AhmedGrati <ahmedgrati1999@gmail.com>

* feat: add generate network policies to client

Signed-off-by: AhmedGrati <ahmedgrati1999@gmail.com>

* update go mod

Signed-off-by: AhmedGrati <ahmedgrati1999@gmail.com>

---------

Signed-off-by: AhmedGrati <ahmedgrati1999@gmail.com>
This commit is contained in:
AhmedGrati 2023-08-24 11:38:21 +01:00 committed by GitHub
parent b83d4d4094
commit ea80734f91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 530 additions and 3 deletions

3
.gitignore vendored
View File

@ -76,3 +76,6 @@ tags
.idea .idea
.DS_Store .DS_Store
# Client Test generated files
client/testdata/generated

View File

@ -138,3 +138,7 @@ install-golangci-lint:
.PHONY: golangci-lint .PHONY: golangci-lint
golangci-lint: install-golangci-lint golangci-lint: install-golangci-lint
$(GOLANGCI_LINT) run -c .golangci.yml --timeout 5m $(GOLANGCI_LINT) run -c .golangci.yml --timeout 5m
.PHONY: test-client
test-client:
go test ./client/...

21
client/client.go Normal file
View File

@ -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
}

248
client/convert.go Normal file
View File

@ -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 ""
}

83
client/convert_test.go Normal file
View File

@ -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))
}
}
}

25
client/options.go Normal file
View File

@ -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
}
}

61
client/options_test.go Normal file
View File

@ -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))
}
}

7
client/testdata/docker-compose.yaml vendored Normal file
View File

@ -0,0 +1,7 @@
version: '3'
services:
web:
image: nginx:latest
ports:
- "80:80"

69
client/types.go Normal file
View File

@ -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
}

1
go.mod
View File

@ -20,6 +20,7 @@ require (
github.com/spf13/viper v1.16.0 github.com/spf13/viper v1.16.0
golang.org/x/tools v0.12.0 golang.org/x/tools v0.12.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.4.0
k8s.io/api v0.28.0 k8s.io/api v0.28.0
k8s.io/apimachinery v0.28.0 k8s.io/apimachinery v0.28.0
) )

1
go.sum
View File

@ -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= 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.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= 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-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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -22,6 +22,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/runtime"
"os" "os"
@ -143,20 +144,22 @@ func ValidateFlags(args []string, cmd *cobra.Command, opt *kobject.ConvertOption
} }
// ValidateComposeFile validates the compose file provided for conversion // ValidateComposeFile validates the compose file provided for conversion
func ValidateComposeFile(opt *kobject.ConvertOptions) { func ValidateComposeFile(opt *kobject.ConvertOptions) error {
if len(opt.InputFiles) == 0 { if len(opt.InputFiles) == 0 {
for _, name := range DefaultComposeFiles { for _, name := range DefaultComposeFiles {
_, err := os.Stat(name) _, err := os.Stat(name)
if err != nil { if err != nil {
log.Debugf("'%s' not found: %v", name, err) log.Debugf("'%s' not found: %v", name, err)
return err
} else { } else {
opt.InputFiles = []string{name} opt.InputFiles = []string{name}
return return nil
} }
} }
log.Fatal("No 'docker-compose' file found") log.Fatal("No 'docker-compose' file found")
} }
return nil
} }
func validateControllers(opt *kobject.ConvertOptions) { func validateControllers(opt *kobject.ConvertOptions) {
@ -202,7 +205,7 @@ func validateControllers(opt *kobject.ConvertOptions) {
} }
// Convert transforms docker compose or dab file to k8s objects // 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) validateControllers(&opt)
// loader parses input from file into komposeObject. // loader parses input from file into komposeObject.
@ -236,6 +239,7 @@ func Convert(opt kobject.ConvertOptions) {
if err != nil { if err != nil {
log.Fatalf(err.Error()) log.Fatalf(err.Error())
} }
return objects, err
} }
// Convenience method to return the appropriate Transformer based on // Convenience method to return the appropriate Transformer based on