diff --git a/.github/workflows/daisctl-pr.yml b/.github/workflows/daisctl-pr.yml new file mode 100644 index 00000000..c24d8596 --- /dev/null +++ b/.github/workflows/daisctl-pr.yml @@ -0,0 +1,84 @@ +name: Daisctl PRs + +env: + WORKDIR: tools/daisctl + +on: + push: + branches: + - "main" + pull_request: + paths: + - "tools/daisctl/**/*.go" + - "tools/daisctl/go.*" + - "tools/daisctl/Makefile" + - "./github/workflows/daisctl-pr.yml" + +jobs: + tidy: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ${{ env.WORKDIR }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: '${{ env.WORKDIR }}/go.mod' + cache: true + - name: Check go mod + run: | + go mod tidy + git diff --exit-code go.mod + git diff --exit-code go.sum + + lint: + needs: tidy + runs-on: ubuntu-latest + defaults: + run: + working-directory: ${{ env.WORKDIR }} + steps: + - uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: '${{ env.WORKDIR }}/go.mod' + cache: true + - name: lint + uses: golangci/golangci-lint-action@v6.0.1 + with: + version: latest + working-directory: ${{ env.WORKDIR }} + + test: + needs: lint + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + include: + - os: ubuntu-latest + shell: bash + test-cmd: make test + - os: macos-latest + shell: bash + test-cmd: make test + - os: windows-latest + shell: powershell + test-cmd: make.exe test + name: Test - ${{ matrix.os }} + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.WORKDIR }} + steps: + - uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: '${{ env.WORKDIR }}/go.mod' + cache: true + - name: Run tests + run: ${{ matrix.test-cmd }} diff --git a/.github/workflows/daisctl-release.yml b/.github/workflows/daisctl-release.yml new file mode 100644 index 00000000..fdd0ce2f --- /dev/null +++ b/.github/workflows/daisctl-release.yml @@ -0,0 +1,33 @@ +name: Release Daisctl + +env: + WORKDIR: tools/daisctl + +on: + push: + tags: + - "daisctl-*" + +permissions: + contents: write + id-token: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-go@v5 + with: + go-version-file: '${{ env.WORKDIR }}/go.mod' + cache: true + - uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: 'latest' + args: release --clean + working-directory: ${{ env.WORKDIR }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/tools/daisctl/.gitignore b/tools/daisctl/.gitignore new file mode 100644 index 00000000..57fd6d2c --- /dev/null +++ b/tools/daisctl/.gitignore @@ -0,0 +1,33 @@ +# Output of the `go build` command +*.out + +# Directories for Go tools +bin/ +dist/ + +# Go test binary and coverage profiles +*.test +*.coverprofile +*.cov +*.out +*.cover + +# IDE and editor-specific files +.vscode/ +.idea/ + + +# Git-specific files +.gitattributes +.gitmodules +.git/ + +# Common build system files +.env + +# Godoc +.doc/ + +# System files +.DS_Store +Thumbs.db diff --git a/tools/daisctl/.goreleaser.yaml b/tools/daisctl/.goreleaser.yaml new file mode 100644 index 00000000..0fe806a9 --- /dev/null +++ b/tools/daisctl/.goreleaser.yaml @@ -0,0 +1,51 @@ +project_name: daisctl + +builds: + - binary: dais + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm64 + flags: + - -trimpath + ldflags: + - -s -w -X github.com/altinn/altinn-platform/daisctl/internal/version.version={{ .Version }} + - -X github.com/altinn/altinn-platform/daisctl/internal/version.commit={{ .Commit }} + - -X github.com/altinn/altinn-platform/daisctl/internal/version.date={{ .CommitDate }} + + +changelog: + sort: asc + use: github + filters: + exclude: + - "^test:" + - "^chore" + - "merge conflict" + - Merge pull request + - Merge remote-tracking branch + - Merge branch + - go mod tidy + include: + - cli + - daisctl + +archives: + - format: tar.gz + wrap_in_directory: true + format_overrides: + - goos: windows + format: zip + name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + files: + - LICENSE + - README.md + +source: + enabled: false + # name_template: '{{ .ProjectName }}-{{ .Version }}-source' diff --git a/tools/daisctl/LICENSE b/tools/daisctl/LICENSE new file mode 100644 index 00000000..dac8d736 --- /dev/null +++ b/tools/daisctl/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 The Norwegian Digitalisation Agency + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/tools/daisctl/Makefile b/tools/daisctl/Makefile new file mode 100644 index 00000000..69fa4833 --- /dev/null +++ b/tools/daisctl/Makefile @@ -0,0 +1,53 @@ +.DEFAULT_GOAL = all + +BINARY = dais +PROJECT_NAME = github.com/altinn/altinn-platform/daisctl +VERSION_PKG = github.com/altinn/altinn-platform/daisctl/internal/version +CONCURRENCY := 4 +GO := go +TAGS ?= "" +COMMIT := $(shell git rev-parse HEAD) +VERSION := "dev" +DATE := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ') + + +# Check if windows +ifeq ($(OS),Windows_NT) + BINARY := $(BINARY).exe +endif + +all: test build +build: + $(GO) build -o $(BINARY) -ldflags "-X $(VERSION_PKG).version=$(VERSION) -X $(VERSION_PKG).commit=$(COMMIT) -X $(VERSION_PKG).date=$(DATE) -s -w" -v + +tidy: + ${GO} mod tidy + +deps: + $(GO) get -tags ${TAGS} -t ./... + make tidy + +test: build + ${GO} test --timeout 30m -short -count 1 -parallel ${CONCURRENCY} ./... + +coverage: build + ${GO} test --timeout 30m -count 1 -coverpkg=${PROJECT_NAME}/... -race -coverprofile=coverage.out -parallel ${CONCURRENCY} ./... + ${GO} tool cover -html=coverage.out -o=coverage-report.html + printf "Coverage report available at coverage-report.html\n" + +clean: + $(GO) clean + rm -f $(BINARY) + rm -f coverage* + rm -rf dist + +format: + $(GO) fmt ./... + +lint: + golangci-lint run + +snapshot: + goreleaser build --snapshot --clean + +.PHONY: all build test diff --git a/tools/daisctl/README.md b/tools/daisctl/README.md new file mode 100644 index 00000000..1ce2f289 --- /dev/null +++ b/tools/daisctl/README.md @@ -0,0 +1,80 @@ +# daisctl + +Welcome to **daisctl**! This is a command-line tool built with Go and Cobra that provides various utilities for managing and interacting with our platform. + +## Table of Contents + +- [Installation](#installation) +- [Usage](#usage) +- [Commands](#commands) +- [Contributing](#contributing) +- [License](#license) + +## Installation + +To install daisctl, you can download the latest release from the [releases page](https://github.com/Altinn/altinn-platform/releases) or build it from source. + +### Building from Source + +1. **Clone the repository**: + ```sh + git clone https://github.com/altinn/altinn-platform.git + cd altinn-platform/tools/daisctl + ``` + +2. **Build the application**: + ```sh + make build + ``` + +3. **Add the executable to your PATH** (optional): + - On **Windows**: + Add it to Environment variables in system properties + - On **macOS/Linux**: + ```sh + export PATH=$PATH:/path/to/daisctl + ``` + +## Usage + +After installation, you can use daisctl by running the `dais` command followed by a specific command and options. + +```sh +dais [command] [flags] +``` + +### Example + +To check the version of daisctl: + +```sh +dais version +Dais version: dais-v0.0.1 +Commit: f6e5cacf1029e28a260b4a28fffee85eb4f67aa9 +Build Date: 2024-07-30T10:58:16Z +``` + +## Commands + +The CLI Application currently supports the following commands: + +- **version**: Displays the current version of daisctl. + ```sh + dais version + ``` + +- **releases**: Lists the current releases running. + ```sh + dais releases --app my-app + dais r + dais rel --app=myapp + ``` + +- **help**: Displays help information for daisctl and its commands. + ```sh + dais help + ``` + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/tools/daisctl/cmd/releases.go b/tools/daisctl/cmd/releases.go new file mode 100644 index 00000000..46054350 --- /dev/null +++ b/tools/daisctl/cmd/releases.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "fmt" + + "github.com/altinn/altinn-platform/daisctl/pkg/altinn" + "github.com/altinn/altinn-platform/daisctl/pkg/kube" + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + + "github.com/charmbracelet/bubbles/table" +) + +// Flags for the releases cmd +var appName string + +var releasesCmd = &cobra.Command{ + Use: "releases", + Aliases: []string{"r", "rel"}, + Short: "Display information of all releases per app", + RunE: func(cmd *cobra.Command, args []string) error { + + d, err := altinn.GetAllDeployments() + if err != nil { + return err + } + + if appName != "" { + appVersions := d.GetAppVersions(appName) + if appVersions == nil { + fmt.Printf("No releases found for app: %s\n", appName) + return nil + } + fmt.Println(setupTable(appVersions).View()) + } else { + fmt.Println(setupTable(d.Apps).View()) + } + return nil + + }, +} + +func setupTable(deployments map[string]*kube.AppVersions) table.Model { + + columns := []table.Column{ + {Title: "App", Width: 30}, + {Title: "at21", Width: 10}, + {Title: "at22", Width: 10}, + {Title: "at23", Width: 10}, + {Title: "at24", Width: 10}, + {Title: "yt01", Width: 10}, + {Title: "tt02", Width: 10}, + {Title: "production", Width: 10}, + } + var rows []table.Row + + for _, d := range deployments { + rows = append(rows, table.Row{ + d.AppName, + d.Versions["at21"], + d.Versions["at22"], + d.Versions["at23"], + d.Versions["at24"], + d.Versions["yt01"], + d.Versions["tt02"], + d.Versions["production"], + }) + } + + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(len(rows)+1), + ) + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("241")). + BorderBottom(true). + Bold(true) + t.SetStyles(s) + return t +} + +func init() { + releasesCmd.Flags().StringVar(&appName, "app", "", "App name to display its versions, e.g --app=myapp, --app myapp") +} diff --git a/tools/daisctl/cmd/root.go b/tools/daisctl/cmd/root.go new file mode 100644 index 00000000..289cdb11 --- /dev/null +++ b/tools/daisctl/cmd/root.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "dais", + Short: "Daisctl is a CLI tool for managing and interacting with the Dais platform", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func init() { + // Completion not needed at the moment + rootCmd.CompletionOptions.DisableDefaultCmd = true + rootCmd.AddCommand(releasesCmd) + rootCmd.AddCommand(versionCmd) +} diff --git a/tools/daisctl/cmd/version.go b/tools/daisctl/cmd/version.go new file mode 100644 index 00000000..3253a39f --- /dev/null +++ b/tools/daisctl/cmd/version.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "fmt" + + "github.com/altinn/altinn-platform/daisctl/internal/version" + "github.com/spf13/cobra" +) + +var BuildInfo version.VersionInfo + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the build information for daisctl", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Printf("Dais version: %s\nCommit: %s\nBuild Date: %s\n", BuildInfo.Version, BuildInfo.Commit, BuildInfo.Date) + return nil + }, +} diff --git a/tools/daisctl/go.mod b/tools/daisctl/go.mod new file mode 100644 index 00000000..18fcb938 --- /dev/null +++ b/tools/daisctl/go.mod @@ -0,0 +1,35 @@ +module github.com/altinn/altinn-platform/daisctl + +go 1.22.0 + +require ( + github.com/charmbracelet/bubbles v0.18.0 + github.com/charmbracelet/lipgloss v0.9.1 + github.com/google/go-cmp v0.6.0 + github.com/spf13/cobra v1.8.0 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v0.26.4 // indirect + github.com/charmbracelet/x/ansi v0.1.2 // indirect + github.com/charmbracelet/x/input v0.1.0 // indirect + github.com/charmbracelet/x/term v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.1.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/tools/daisctl/go.sum b/tools/daisctl/go.sum new file mode 100644 index 00000000..befc3caf --- /dev/null +++ b/tools/daisctl/go.sum @@ -0,0 +1,63 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40= +github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0= +github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= +github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= +github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= +github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= +github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= +github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= +github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= +github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +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= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +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/tools/daisctl/internal/version/version.go b/tools/daisctl/internal/version/version.go new file mode 100644 index 00000000..9b56308f --- /dev/null +++ b/tools/daisctl/internal/version/version.go @@ -0,0 +1,23 @@ +package version + +var ( + // Dummy dev values, replaced during build + // with ldflags + version = "dev" + commit = "" + date = "1970-01-01T00:00:00Z" +) + +type VersionInfo struct { + Version string + Commit string + Date string +} + +func Get() VersionInfo { + return VersionInfo{ + Version: version, + Commit: commit, + Date: date, + } +} diff --git a/tools/daisctl/main.go b/tools/daisctl/main.go new file mode 100644 index 00000000..69baa443 --- /dev/null +++ b/tools/daisctl/main.go @@ -0,0 +1,14 @@ +/* +Copyright © 2024, Altinn +*/ +package main + +import ( + "github.com/altinn/altinn-platform/daisctl/cmd" + "github.com/altinn/altinn-platform/daisctl/internal/version" +) + +func main() { + cmd.BuildInfo = version.Get() + cmd.Execute() +} diff --git a/tools/daisctl/pkg/altinn/deployments.go b/tools/daisctl/pkg/altinn/deployments.go new file mode 100644 index 00000000..b3dbd451 --- /dev/null +++ b/tools/daisctl/pkg/altinn/deployments.go @@ -0,0 +1,79 @@ +package altinn + +import ( + "fmt" + + "github.com/altinn/altinn-platform/daisctl/pkg/kube" + "github.com/altinn/altinn-platform/daisctl/pkg/util" +) + +type Deployments struct { + Apps map[string]*kube.AppVersions +} + +func newDeployments() *Deployments { + return &Deployments{ + Apps: make(map[string]*kube.AppVersions), + } + +} + +func GetAllDeployments() (*Deployments, error) { + d := newDeployments() + + envs, err := GetEnvironments(util.EnvironmentsAPI) + if err != nil { + return nil, err + } + + for _, e := range envs { + kwUrl := fmt.Sprintf("%s/"+util.KubeWrapperAPI+"/Deployments", e.PlatformUrl) + err := d.initAppsData(kwUrl, e) + if err != nil { + return nil, err + } + + //TODO: in the near future there should not be a Daemonsets endpoint + kwUrl = fmt.Sprintf("%s/"+util.KubeWrapperAPI+"/Daemonsets", e.PlatformUrl) + err = d.initAppsData(kwUrl, e) + if err != nil { + return nil, err + } + } + + return d, nil +} + +func (d *Deployments) initAppsData(kwUrl string, e Environment) error { + apps, err := kube.GetAppInfos(kwUrl) + if err != nil { + return err + } + + for _, app := range apps { + if _, exists := d.Apps[app.Release]; !exists { + d.Apps[app.Release] = &kube.AppVersions{ + AppName: app.Release, + Versions: make(map[string]string), + } + } + d.Apps[app.Release].Versions[e.Name] = app.Version + } + + return nil +} + +// GetAppVersions returns the map containing the specified app +func (d *Deployments) GetAppVersions(appName string) map[string]*kube.AppVersions { + if app, exists := d.Apps[appName]; exists { + return map[string]*kube.AppVersions{ + appName: app, + } + } + return nil +} + +// GetAllApps returns all apps and their versions across all environments +func (d *Deployments) GetAllApps() map[string]*kube.AppVersions { + return d.Apps +} diff --git a/tools/daisctl/pkg/altinn/environments.go b/tools/daisctl/pkg/altinn/environments.go new file mode 100644 index 00000000..1065de6c --- /dev/null +++ b/tools/daisctl/pkg/altinn/environments.go @@ -0,0 +1,24 @@ +package altinn + +import "github.com/altinn/altinn-platform/daisctl/pkg/util" + +type Environment struct { + PlatformUrl string `json:"platformUrl"` + Hostname string `json:"hostname"` + AppPrefix string `json:"appPrefix"` + PlatformPrefix string `json:"platformPrefix"` + Name string `json:"name"` + Type string `json:"type"` +} + +type EnvsResp struct { + Environments []Environment `json:"environments"` +} + +func GetEnvironments(url string) ([]Environment, error) { + response, err := util.RequestObject[EnvsResp](url) + if err != nil { + return nil, err + } + return response.Environments, nil +} diff --git a/tools/daisctl/pkg/altinn/environments_test.go b/tools/daisctl/pkg/altinn/environments_test.go new file mode 100644 index 00000000..e1018b24 --- /dev/null +++ b/tools/daisctl/pkg/altinn/environments_test.go @@ -0,0 +1,103 @@ +package altinn + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestGetEnvironments(t *testing.T) { + tests := []struct { + name string + mockResponse string + statusCode int + expectedResult []Environment + expectError bool + }{ + { + name: "Valid Response", + mockResponse: `{ + "environments": [ + { + "platformUrl": "https://a.com", + "hostname": "a.com", + "appPrefix": "apps", + "platformPrefix": "platform", + "name": "dev", + "type": "test" + }, + { + "platformUrl": "https://b.com", + "hostname": "b.com", + "appPrefix": "apps", + "platformPrefix": "platform", + "name": "prod", + "type": "production" + } + ] + }`, + statusCode: http.StatusOK, + expectedResult: []Environment{ + { + PlatformUrl: "https://a.com", + Hostname: "a.com", + AppPrefix: "apps", + PlatformPrefix: "platform", + Name: "dev", + Type: "test", + }, + { + PlatformUrl: "https://b.com", + Hostname: "b.com", + AppPrefix: "apps", + PlatformPrefix: "platform", + Name: "prod", + Type: "production", + }, + }, + expectError: false, + }, + { + name: "Malformed JSON Response", + mockResponse: `{"environments": [ { "platformUrl": "https://a.com", "name": "dev" `, + statusCode: http.StatusOK, + expectedResult: nil, + expectError: true, + }, + { + name: "Server Error", + mockResponse: `Internal Server Error`, + statusCode: http.StatusInternalServerError, + expectedResult: nil, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a test server that returns the mock response + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.statusCode) + // check return value, otherwise lint will complain + if _, err := w.Write([]byte(tt.mockResponse)); err != nil { + t.Errorf("failed to write mock response: %v", err) + } + })) + defer server.Close() + + // Call GetEnvironments with the test server URL + envs, err := GetEnvironments(server.URL) + + if (err != nil) != tt.expectError { + t.Fatalf("Expected error: %v, got: %v", tt.expectError, err) + } + + if diff := cmp.Diff(envs, tt.expectedResult); diff != "" { + t.Errorf("GetEnvironments() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/tools/daisctl/pkg/kube/app.go b/tools/daisctl/pkg/kube/app.go new file mode 100644 index 00000000..b412d143 --- /dev/null +++ b/tools/daisctl/pkg/kube/app.go @@ -0,0 +1,23 @@ +package kube + +import ( + "github.com/altinn/altinn-platform/daisctl/pkg/util" +) + +type AppInfo struct { + Version string `json:"version"` + Release string `json:"release"` +} + +type AppVersions struct { + AppName string + Versions map[string]string // Map of environment to version +} + +func GetAppInfos(url string) ([]AppInfo, error) { + r, err := util.RequestArray[AppInfo](url) + if err != nil { + return nil, err + } + return r, nil +} diff --git a/tools/daisctl/pkg/kube/app_test.go b/tools/daisctl/pkg/kube/app_test.go new file mode 100644 index 00000000..e88d2d79 --- /dev/null +++ b/tools/daisctl/pkg/kube/app_test.go @@ -0,0 +1,86 @@ +package kube + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestGetAppInfos(t *testing.T) { + tests := []struct { + name string + mockResponse string + statusCode int + expectedResult []AppInfo + expectError bool + }{ + { + name: "Valid Response", + mockResponse: `[ + { + "version": "1.0", + "release": "app1" + }, + { + "version": "2.0", + "release": "app2" + } + ]`, + statusCode: http.StatusOK, + expectedResult: []AppInfo{ + {Version: "1.0", Release: "app1"}, + {Version: "2.0", Release: "app2"}, + }, + expectError: false, + }, + { + name: "Malformed JSON Response", + mockResponse: `[{"version": "1.0", "release": "app1"`, + statusCode: http.StatusOK, + expectedResult: nil, + expectError: true, + }, + { + name: "Empty Response", + mockResponse: `[]`, + statusCode: http.StatusOK, + expectedResult: []AppInfo{}, + expectError: false, + }, + { + name: "Server Error", + mockResponse: `Internal Server Error`, + statusCode: http.StatusInternalServerError, + expectedResult: nil, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a test server that returns the mock response + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.statusCode) + // check return value, otherwise lint will complain + if _, err := w.Write([]byte(tt.mockResponse)); err != nil { + t.Errorf("failed to write mock response: %v", err) + } + })) + defer server.Close() + + // Call GetAppInfos with the test server URL + apps, err := GetAppInfos(server.URL) + + if (err != nil) != tt.expectError { + t.Fatalf("Expected error: %v, got: %v", tt.expectError, err) + } + + if diff := cmp.Diff(apps, tt.expectedResult); diff != "" { + t.Errorf("GetAppInfos() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/tools/daisctl/pkg/util/constants.go b/tools/daisctl/pkg/util/constants.go new file mode 100644 index 00000000..d7b854cc --- /dev/null +++ b/tools/daisctl/pkg/util/constants.go @@ -0,0 +1,6 @@ +package util + +const ( + EnvironmentsAPI = "https://altinncdn.no/config/environments.json" + KubeWrapperAPI = "kuberneteswrapper/api/v1" +) diff --git a/tools/daisctl/pkg/util/http.go b/tools/daisctl/pkg/util/http.go new file mode 100644 index 00000000..0b2aed71 --- /dev/null +++ b/tools/daisctl/pkg/util/http.go @@ -0,0 +1,42 @@ +package util + +import ( + "encoding/json" + "net/http" +) + +// Generic request function to handle a single object +func RequestObject[T any](url string) (T, error) { + res, err := http.Get(url) + if err != nil { + var zeroValue T + return zeroValue, err + } + defer res.Body.Close() + + var respInfo T + err = json.NewDecoder(res.Body).Decode(&respInfo) + if err != nil { + var zeroValue T + return zeroValue, err + } + + return respInfo, nil +} + +// Generic request function to handle slices directly +func RequestArray[T any](url string) ([]T, error) { + res, err := http.Get(url) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var respInfo []T + err = json.NewDecoder(res.Body).Decode(&respInfo) + if err != nil { + return nil, err + } + + return respInfo, nil +}