From 547ff5192d9c9d890659da7fb700fa61fea0a869 Mon Sep 17 00:00:00 2001 From: Omar El Malak Date: Thu, 17 Oct 2024 12:28:27 +0200 Subject: [PATCH] first implementation (#1) * initial development * setup commands * add goreleaser config and release pipeline --------- Co-authored-by: Alessandro Cannarella Co-authored-by: Omar El Malak --- .github/workflows/release.yml | 58 ++++++++ .gitignore | 114 ++++++++++++++- .goreleaser.yaml | 44 ++++++ README.md | 56 +++++++ cmd/main.go | 29 ++++ go.mod | 13 ++ go.sum | 16 ++ internal/cliconfig/cliconfig.go | 52 +++++++ internal/cmd/config.go | 59 ++++++++ internal/cmd/launch.go | 250 ++++++++++++++++++++++++++++++++ internal/cmd/root.go | 48 ++++++ internal/cmd/version.go | 19 +++ internal/logger/doc.go | 16 ++ internal/logger/log.go | 101 +++++++++++++ 14 files changed, 872 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yaml create mode 100644 cmd/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/cliconfig/cliconfig.go create mode 100644 internal/cmd/config.go create mode 100644 internal/cmd/launch.go create mode 100644 internal/cmd/root.go create mode 100644 internal/cmd/version.go create mode 100644 internal/logger/doc.go create mode 100644 internal/logger/log.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9d0b5eb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +name: Release + +on: + push: + branches: + - main # Esegue build snapshot per ogni push su main + tags: + - 'v*' # Esegue una release ufficiale quando il tag inizia con 'v' + +env: + GORELEASER_VERSION: v2.3.2 + +jobs: + build: + name: Build and Release for Supported Architectures + runs-on: ubuntu-latest + + steps: + # Checkout del codice + - name: Checkout Repository + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + with: + show-progress: false + + # Setup di Go + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23.2' + + # Installazione di GoReleaser + - name: Install GoReleaser + uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 + with: + version: ${{ env.GORELEASER_VERSION }} + install-only: true + + # Esecuzione di GoReleaser per snapshot build (su main) + - name: Run GoReleaser (Snapshot) + if: github.ref == 'refs/heads/main' + run: goreleaser release --snapshot --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Esecuzione di GoReleaser per rilasci ufficiali (su tag) + - name: Run GoReleaser (Release) + if: startsWith(github.ref, 'refs/tags/v') + run: goreleaser release --clean --skip-validate --skip-publish=false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Salva i binari come artefatti in snapshot build + - name: Upload artifact (Snapshot) + if: github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v3 + with: + name: artifacts + path: bin/ diff --git a/.gitignore b/.gitignore index 6f72f89..2062ba6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,14 @@ +# Custom gitignore rules + +bin/ +coverage.txt + +# End of Custom gitignore rules + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,windows,macos,go +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,linux,windows,macos,go + +### 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 # @@ -19,7 +30,104 @@ # Go workspace file go.work -go.work.sum -# env file -.env +### 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 + +### 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/visualstudiocode,linux,windows,macos,go + + +mipyconfig.json +environment +config_path.txt +dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..7ff3cb9 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,44 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 2 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + - go generate ./... + +builds: + - main: ./cmd/main.go + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm + - arm64 + - "386" + goarm: + - "6" + - "7" + +archives: +- format: binary + name_template: >- + {{ .Binary }}- + {{- .Os }}- + {{- .Arch }}{{ with .Arm }}v{{ . }}{{ end }} + {{- with .Mips }}-{{ . }}{{ end }} + {{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }} + +checksum: + name_template: checksums.txt diff --git a/README.md b/README.md index e30659c..905fb6d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,58 @@ # mipy Cli helper to help in IaC provisioning through the Console pipelines + +## Setup + +The mipy cli makes use of a configmap file as this one: +```json +{ + "basePath": "string", + "templates": [ + { + "type": "enum", + "id": "string", + "cicdProvider": "string", // for now only "azure" is supported + "cicdProviderBaseUrl": "string", + "azureOrganization": "string", + "azureProject": "string", + "terraformPipelineId": "string" + } + ], + "logLevel": "string" +} +``` + +## Commands + +### version + +``` +mipy version +``` + +### help + +``` +mipy help +``` + +### config + +flags are: +- get +- set [PATH] +- --help + +``` +mipy config set mipy.json +``` + +### launch + +flags are: +- --environment (-e): required +- --cr-list +- --parallel +- --error-code +- --debug +- --dry-run \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..e75305b --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,29 @@ +// Copyright 2024-2024 Mipy Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "os" + + "github.com/mia-platform/mipy/internal/cmd" +) + +func main() { + rootCmd := cmd.NewRootCommand() + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } + os.Exit(0) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..df33721 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/mia-platform/mipy + +go 1.23.2 + +require github.com/mia-platform/miactl v0.15.0 + +require ( + github.com/go-logr/logr v1.4.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8430ad9 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mia-platform/miactl v0.15.0 h1:tCvFnkqMjit+Y7giZaF3ZVPXKvz1eG3Cen/CsXA4QfI= +github.com/mia-platform/miactl v0.15.0/go.mod h1:QOflNDd0wdfmB6TvTe7XWG8KTDo7R5RVIlQvMqQY9DU= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +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.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cliconfig/cliconfig.go b/internal/cliconfig/cliconfig.go new file mode 100644 index 0000000..b987345 --- /dev/null +++ b/internal/cliconfig/cliconfig.go @@ -0,0 +1,52 @@ +package cliconfig + +import ( + "encoding/json" + "os" +) + +type Config struct { + BasePath string `json:"basePath"` + Templates []Template `json:"templates"` + LogLevel string `json:"logLevel"` +} + +type Template struct { + Type string `json:"type"` + Id string `json:"id"` + CICDProvider string `json:"cicdProvider"` + CICDBaseUrl string `json:"cicdProviderBaseUrl"` + AzureOrganization string `json:"azureOrganization"` + AzureProject string `json:"azureProject"` + TerraformPipelineID string `json:"terraformPipelineId"` +} + +func loadPreferredConfigPath() (string, error) { + data, err := os.ReadFile("config_path.txt") + if err != nil { + return "mipyconfig.json", nil + } + return string(data), nil +} + +func SavePreferredConfigPath(path string) error { + return os.WriteFile("config_path.txt", []byte(path), 0644) +} + +func ReadConfigFile() (*Config, error) { + path, err := loadPreferredConfigPath() + if err != nil { + return nil, err + } + + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + config := new(Config) + if err := json.Unmarshal(data, &config); err != nil { + return nil, err + } + + return config, nil +} diff --git a/internal/cmd/config.go b/internal/cmd/config.go new file mode 100644 index 0000000..009d3fa --- /dev/null +++ b/internal/cmd/config.go @@ -0,0 +1,59 @@ +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/mia-platform/mipy/internal/cliconfig" + "github.com/spf13/cobra" +) + +func ConfigGetCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "get", + Short: "get current config", + Run: func(cmd *cobra.Command, _ []string) { + fmt.Println("Get current configuration:") + config, err := cliconfig.ReadConfigFile() + jsonToPrint, err := json.MarshalIndent(config, "", " ") + if err != nil { + fmt.Println("Error:", err) + return + } + fmt.Println(string(jsonToPrint)) + }, + } + return cmd +} + +func ConfigSetCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "set [PATH]", + Short: "set config given path", + RunE: func(cmd *cobra.Command, args []string) error { + path := args[0] + fmt.Printf("Setting configuration from file: %s\n", path) + if err := cliconfig.SavePreferredConfigPath(path); err != nil { + return errors.New("Error while retrieving new configuration") + } + return nil + }, + } + + return cmd +} + +func ConfigCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "config", + Short: "Set or get config", + } + + cmd.AddCommand( + ConfigGetCmd(), + ConfigSetCmd(), + ) + + return cmd +} diff --git a/internal/cmd/launch.go b/internal/cmd/launch.go new file mode 100644 index 0000000..f03b43a --- /dev/null +++ b/internal/cmd/launch.go @@ -0,0 +1,250 @@ +package cmd + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/fs" + "log" + "net/http" + "os" + "path/filepath" + + "github.com/joho/godotenv" + "github.com/mia-platform/mipy/internal/cliconfig" + "github.com/spf13/cobra" +) + +var crList []string +var parallel bool +var errorCode int +var debug bool +var dryRun bool +var environment string +var username string +var password string + +type CRInfo struct { + Path string + TemplateType string + CICDProvider string + CICDBaseUrl string + CICDOrganization string + CICDProject string + TerraformPipelineID string +} + +type TerraformRequestBody struct { + Resources struct { + Repositories struct { + Self struct { + RefName string `json:"refName"` + } `json:"self"` + } `json:"repositories"` + } `json:"resources"` + TemplateParameters struct { + DebugMode bool `json:"DEBUG_MODE"` + TerraformAutoApprove string `json:"TERRAFORM_AUTO_APPROVE"` + TerraformAction string `json:"TERRAFORM_ACTION"` + } `json:"templateParameters"` + Variables struct { + AzureSubscriptionID struct { + IsSecret bool `json:"isSecret"` + Value string `json:"value"` + } `json:"AZURE_SUBSCRIPTION_ID"` + AzureTenantID struct { + IsSecret bool `json:"isSecret"` + Value string `json:"value"` + } `json:"AZURE_TENANT_ID"` + TerraformVariables struct { + IsSecret bool `json:"isSecret"` + Value string `json:"value"` + } `json:"TERRAFORM_VARIABLES"` + } `json:"variables"` +} + +func azureTerraformCR(cr CRInfo, user string, password string, configContent string, envVars map[string]string) error { + azSubscriptionID := envVars["AZURE_SUBSCRIPTION_ID"] + azTenantID := envVars["AZURE_TENANT_ID"] + terraformAction := envVars["ACTION"] + terraformAutoApprove := envVars["AUTO_APPROVE"] + + requestBody := TerraformRequestBody{} + requestBody.Resources.Repositories.Self.RefName = "refs/heads/{branch}" // TODO default 'main' + requestBody.TemplateParameters.DebugMode = true + requestBody.TemplateParameters.TerraformAutoApprove = terraformAutoApprove + requestBody.TemplateParameters.TerraformAction = terraformAction + requestBody.Variables.AzureSubscriptionID.IsSecret = false + requestBody.Variables.AzureSubscriptionID.Value = azSubscriptionID + requestBody.Variables.AzureTenantID.IsSecret = false + requestBody.Variables.AzureTenantID.Value = azTenantID + requestBody.Variables.TerraformVariables.IsSecret = false + requestBody.Variables.TerraformVariables.Value = configContent + + jsonData, err := json.Marshal(requestBody) + if err != nil { + fmt.Println("Error mashal error") + return err + } + auth := base64.StdEncoding.EncodeToString([]byte(user + ":" + password)) + + url := fmt.Sprintf("%s/%s/%s/_apis/pipelines/%s/runs?api-version=7.1", cr.CICDBaseUrl, cr.CICDOrganization, cr.CICDProject, cr.TerraformPipelineID) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + log.Fatalf("Error creating request: %v", err) + return err + } + + // Set headers + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Basic "+auth) + + // Send the request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Fatalf("Error sending request: %v", err) + return err + } + defer resp.Body.Close() + + // Print the status code + fmt.Printf("Response Status Code: %d\n", resp.StatusCode) + + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Error reading response body: %v", err) + } + fmt.Println("Response Body:", string(responseBody)) + return nil +} + +func handleTerraformCR(cr CRInfo, user string, password string) error { + fmt.Printf("Handle template %s\n", cr.Path) + configFilePath := filepath.Join(cr.Path, "configs.tf") + variablesFilePath := filepath.Join(cr.Path, "variables.env") + + configContent, err := os.ReadFile(configFilePath) + if err != nil { + return fmt.Errorf("error reading config.tf: %v", err) + } + + encodedConfigContent := base64.StdEncoding.EncodeToString(configContent) + + // Read the variables.env file + envVars, err := godotenv.Read(variablesFilePath) + if err != nil { + return fmt.Errorf("error reading variables.env: %v", err) + } + + if cr.CICDProvider != "azure" { + return fmt.Errorf("CICD provider not supported") + } + return azureTerraformCR(cr, user, password, encodedConfigContent, envVars) +} + +func launchCR(cr CRInfo, user string, password string) error { + if cr.TemplateType == "terraform" { + err := handleTerraformCR(cr, user, password) + if err != nil { + return err + } + } else { + fmt.Println("Template type %s not implemented", cr.TemplateType) + return fmt.Errorf("Template type not implemented") + } + + return nil +} + +func getCRInfos(basePath string, template cliconfig.Template, environment string) ([]CRInfo, error) { + var crInfos []CRInfo + searchPath := filepath.Join(basePath, template.Id, "environment", environment) + + err := filepath.WalkDir(searchPath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() && path != searchPath { + crInfo := CRInfo{ + Path: path, + TemplateType: template.Type, + CICDProvider: template.CICDProvider, + CICDBaseUrl: template.CICDBaseUrl, + TerraformPipelineID: template.TerraformPipelineID, + } + + if template.CICDProvider == "azure" { + crInfo.CICDOrganization = template.AzureOrganization + crInfo.CICDProject = template.AzureProject + + } + crInfos = append(crInfos, crInfo) + } + return nil + }) + if err != nil { + return nil, err + } + + return crInfos, nil +} + +func getCRsToLaunch(config cliconfig.Config, environment string) ([]CRInfo, error) { + var crs []CRInfo + var err error + for _, template := range config.Templates { + crInfos, err := getCRInfos(config.BasePath, template, environment) + if err != nil { + fmt.Println(err) + } + crs = append(crs, crInfos...) + } + return crs, err +} + +func LaunchCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "launch", + Short: "Launch", + RunE: func(cmd *cobra.Command, _ []string) error { + // get configuration + config, err := cliconfig.ReadConfigFile() + if err != nil { + fmt.Errorf("Failed get templates from configuration") + } + crInfos, err := getCRsToLaunch(*config, environment) + fmt.Println(crInfos) + + if dryRun == true { + return nil + } + + for _, crInfo := range crInfos { + err = launchCR(crInfo, username, password) + if err != nil { + fmt.Println(err) + } + } + + // get all cr for each template id in config + return nil + }, + } + + cmd.Flags().StringVarP(&username, "username", "u", "", "Username for auth to cicd provider") + cmd.Flags().StringVarP(&password, "password", "p", "", "Password for auth to cicd provider") + cmd.Flags().StringSliceVar(&crList, "cr-list", []string{}, "NOT IMPLEMENTED YET: List of CRs to launch") + cmd.Flags().BoolVar(¶llel, "parallel", false, "NOT IMPLEMENTED YET: Launch CRs in parallel") + cmd.Flags().IntVar(&errorCode, "error-code", 500, "Error code to trigger on failure") + cmd.Flags().BoolVar(&debug, "debug", false, "NOT IMPLEMENTED YET: Enable debug mode") + cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Preview CRs without executing") + cmd.Flags().StringVarP(&environment, "environment", "e", "", "Environment to deploy") + cmd.MarkFlagRequired("environment") + cmd.MarkFlagRequired("username") + cmd.MarkFlagRequired("password") + return cmd +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go new file mode 100644 index 0000000..42207f6 --- /dev/null +++ b/internal/cmd/root.go @@ -0,0 +1,48 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + "os" + + "github.com/go-logr/logr" + "github.com/mia-platform/mipy/internal/logger" + "github.com/spf13/cobra" +) + +func NewRootCommand() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "mipy", + Short: "Mia-Platform Infrastructure Provisioning Helper", + Long: `Find more information at: https://github.com/mia-platform/mipy`, + } + + // initialize clioptions and setup during initialization + // options := clioptions.NewCLIOptions() + + logger := logger.NewLogger(os.Stderr) + rootCmd.SetContext(logr.NewContext(context.Background(), logger)) + + // add sub commands + rootCmd.AddCommand( + ConfigCmd(), + LaunchCmd(), + VersionCmd(), + ) + + return rootCmd +} diff --git a/internal/cmd/version.go b/internal/cmd/version.go new file mode 100644 index 0000000..b7bf1b8 --- /dev/null +++ b/internal/cmd/version.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func VersionCmd() *cobra.Command { + var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the version of mipy", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("TO IMPLEMENT mipy vX.Y.Z") + }, + } + + return versionCmd +} diff --git a/internal/logger/doc.go b/internal/logger/doc.go new file mode 100644 index 0000000..2dfc8d2 --- /dev/null +++ b/internal/logger/doc.go @@ -0,0 +1,16 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logger diff --git a/internal/logger/log.go b/internal/logger/log.go new file mode 100644 index 0000000..4735933 --- /dev/null +++ b/internal/logger/log.go @@ -0,0 +1,101 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package logger + +import ( + "fmt" + "io" + + "github.com/go-logr/logr" +) + +var _ logr.LogSink = &stdSink{} + +var LogLevel int + +type stdSink struct { + name string + writer io.Writer + callDepth int +} + +func (sink *stdSink) Init(info logr.RuntimeInfo) { + sink.callDepth = info.CallDepth +} + +func (sink *stdSink) WithName(name string) logr.LogSink { + return &stdSink{ + name: fmt.Sprintf("%s.%s", sink.name, name), + writer: sink.writer, + callDepth: sink.callDepth, + } +} + +func (sink *stdSink) WithValues(_ ...any) logr.LogSink { + // TODO: actually get and use the values + return &stdSink{ + name: sink.name, + writer: sink.writer, + callDepth: sink.callDepth, + } +} + +func (sink *stdSink) Enabled(level int) bool { + return LogLevel >= level +} + +func (sink *stdSink) Error(err error, msg string, kvs ...any) { + // TODO: handle error as an additional value when we will support them + newMsg := fmt.Sprintf("%s: %s", msg, err) + sink.Info(0, newMsg, kvs...) +} + +func (sink *stdSink) Info(_ int, msg string, _ ...any) { + fmt.Fprintf(sink.writer, "%s: %s", sink.name, msg) + fmt.Fprintln(sink.writer) +} + +func NewLogger(w io.Writer) logr.Logger { + sink := &stdSink{ + name: "miactl", + writer: w, + } + + return logr.New(sink) +} + +func NewTestLogger(w io.Writer, level int) logr.Logger { + sink := &testSink{ + stdSink: stdSink{ + name: "test", + writer: w, + }, + level: level, + } + + return logr.New(sink) +} + +var _ logr.LogSink = &testSink{} + +type testSink struct { + stdSink + level int +} + +func (sink *testSink) Enabled(level int) bool { + return sink.level >= level +}