diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..de0f009 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +pkg/** linguist-generated diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3f068d9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,14 @@ +on: + pull_request: + types: [opened, reopened, synchronize, labeled] +jobs: + test: + runs-on: cdk-large + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: 1.22.0 + - name: go test + run: go test ./... + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8cc5e71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,251 @@ +ctl +ctl.iml +# Created by https://www.toptal.com/developers/gitignore/api/go,intellij,visualstudiocode,vim,linux,macos,windows +# Edit at https://www.toptal.com/developers/gitignore?templates=go,intellij,visualstudiocode,vim,linux,macos,windows + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/go,intellij,visualstudiocode,vim,linux,macos,windows diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/cmd/apply.go b/cmd/apply.go new file mode 100644 index 0000000..4626e4a --- /dev/null +++ b/cmd/apply.go @@ -0,0 +1,38 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "fmt" + "github.com/conduktor/ctl/resource" + "github.com/spf13/cobra" + "os" +) + +var filePath *string + +// applyCmd represents the apply command +var applyCmd = &cobra.Command{ + Use: "apply", + Short: "upsert a resource on Conduktor", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + resources, error := resource.FromFile(*filePath) + if error != nil { + fmt.Fprintf(os.Stderr, "%s\n", error) + os.Exit(1) + } + fmt.Println(resources) + }, +} + +func init() { + rootCmd.AddCommand(applyCmd) + + // Here you will define your flags and configuration settings. + filePath = applyCmd. + PersistentFlags().StringP("file", "f", "", "Specify the file to apply") + + applyCmd.MarkPersistentFlagRequired("file") +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..8d0560b --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,31 @@ +/* +Copyright © 2024 NAME HERE +*/ +package cmd + +import ( + "github.com/spf13/cobra" + "os" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "conduktor", + Short: "command line tools for conduktor", + Long: ``, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3dc4075 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/conduktor/ctl + +go 1.22.0 + +require github.com/spf13/cobra v1.8.0 + +require ( + github.com/ghodss/yaml v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3b3786a --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a18ab9b --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/conduktor/ctl/cmd" + +func main() { + cmd.Execute() +} diff --git a/resource/resource.go b/resource/resource.go new file mode 100644 index 0000000..dfcb75f --- /dev/null +++ b/resource/resource.go @@ -0,0 +1,81 @@ +package resource + +import ( + "bytes" + "encoding/json" + "fmt" + yamlJson "github.com/ghodss/yaml" + yaml "gopkg.in/yaml.v3" + "io" + "os" +) + +type Resource struct { + Json []byte + Kind string + Name string + ApiVersion string +} + +func (r Resource) String() string { + return fmt.Sprintf(`version: %s, kind: %s, name: %s, json: '%s'`, r.ApiVersion, r.Kind, r.Name, string(r.Json)) +} + +type yamlRoot struct { + ApiVersion string + Kind string + Metadata metadata +} + +type metadata struct { + Name string +} + +func FromFile(path string) ([]Resource, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + return FromByte(data) +} + +func FromByte(data []byte) ([]Resource, error) { + reader := bytes.NewReader(data) + var yamlData interface{} + results := make([]Resource, 0, 2) + d := yaml.NewDecoder(reader) + for { + err := d.Decode(&yamlData) + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + yamlByte, err := yaml.Marshal(yamlData) + if err != nil { + return nil, err + } + result, err := yamlByteToResource([]byte(yamlByte)) + if err != nil { + return nil, err + } + results = append(results, result) + } + return results, nil +} + +func yamlByteToResource(data []byte) (Resource, error) { + jsonByte, err := yamlJson.YAMLToJSON(data) + if err != nil { + return Resource{}, nil + } + + var yamlRoot yamlRoot + err = json.Unmarshal(jsonByte, &yamlRoot) + if err != nil { + return Resource{}, nil + } + + return Resource{Json: jsonByte, Kind: yamlRoot.Kind, Name: yamlRoot.Metadata.Name, ApiVersion: yamlRoot.ApiVersion}, nil +} diff --git a/resource/resource_test.go b/resource/resource_test.go new file mode 100644 index 0000000..3a2176a --- /dev/null +++ b/resource/resource_test.go @@ -0,0 +1,87 @@ +package resource + +import ( + "testing" +) + +func TestFromByteForOneResourceWithValidResource(t *testing.T) { + yamlByte := []byte(` +# comment +--- +apiVersion: v1 +kind: Topic +metadata: + name: abc.myTopic +spec: + replicationFactor: 1 +--- +apiVersion: v2 +kind: ConsumerGroup +metadata: + name: cg1 +`) + + results, err := FromByte(yamlByte) + if err != nil { + t.Error(err) + } + if len(results) != 2 { + t.Errorf("results expected1 of length 2, got length %d", len(results)) + } + + result1 := results[0] + expected1 := Resource{ + ApiVersion: "v1", + Kind: "Topic", + Name: "abc.myTopic", + Json: []byte(`{"apiVersion":"v1","kind":"Topic","metadata":{"name":"abc.myTopic"},"spec":{"replicationFactor":1}}`), + } + + if result1.Name != expected1.Name { + t.Errorf("Expected name %s got %s", expected1.Name, result1.Name) + } + + if result1.Kind != expected1.Kind { + t.Errorf("Expected name %s got %s", expected1.Kind, result1.Kind) + } + + if result1.ApiVersion != expected1.ApiVersion { + t.Errorf("Expected name %s got %s", expected1.ApiVersion, result1.ApiVersion) + } + + expectedJsonString1 := string(expected1.Json) + resultJsonString1 := string(result1.Json) + if expectedJsonString1 != resultJsonString1 { + t.Errorf("Expected json %s got %s", expectedJsonString1, resultJsonString1) + } + + result2 := results[1] + expected2 := Resource{ + ApiVersion: "v2", + Kind: "ConsumerGroup", + Name: "cg1", + Json: []byte(`{"apiVersion":"v2","kind":"ConsumerGroup","metadata":{"name":"cg1"}}`), + } + + if result2.Name != expected2.Name { + t.Errorf("Expected name %s got %s", expected2.Name, result2.Name) + } + + if result2.Kind != expected2.Kind { + t.Errorf("Expected name %s got %s", expected2.Kind, result2.Kind) + } + + if result2.ApiVersion != expected2.ApiVersion { + t.Errorf("Expected name %s got %s", expected2.ApiVersion, result2.ApiVersion) + } + + expectedJsonString2 := string(expected2.Json) + resultJsonString2 := string(result2.Json) + if expectedJsonString2 != resultJsonString2 { + t.Errorf("Expected json %s got %s", expectedJsonString2, resultJsonString2) + } +} + +func TestFromByte(t *testing.T) { + +} diff --git a/test.yaml b/test.yaml new file mode 100644 index 0000000..60a79ba --- /dev/null +++ b/test.yaml @@ -0,0 +1,25 @@ +# topics.yml +--- +apiVersion: v1 +kind: Topic +metadata: + name: abc.myTopic +spec: + replicationFactor: 1 + partitions: 3 + configs: + min.insync.replicas: 1 + cleanup.policy: delete + retention.ms: 604800000 +--- +apiVersion: v1 +kind: Topic +metadata: + name: abcd.myTopicWrong +spec: + replicationFactor: 1 + partitions: 3 + configs: + min.insync.replicas: 1 + cleanup.policy: delete + retention.ms: 604800000