diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 2fc35d95..3410bd21 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -15,6 +15,10 @@ on: schedule: - cron: 0 5 * * 1 # Run every monday at 5 UTC +env: + GO_VERSION: 1.21.1 + GORELEASER_VERSION: v1.21.2 + jobs: codeql: runs-on: ubuntu-latest @@ -25,6 +29,16 @@ jobs: uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 with: show-progress: false + - name: Setup Golang + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version: ${{ env.GO_VERSION }} + cache: false + - name: Setup Goreleaser + uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 + with: + version: ${{ env.GORELEASER_VERSION }} + install-only: true - name: Initialize CodeQL uses: github/codeql-action/init@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d5bea30..2eddf5af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,11 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - marketplace list command - marketplace get command - environment list command -- pod list command -- cronjob list command -- job list command -- deployment list command -- service list command +- runtime list resources command - events command - version command diff --git a/docs/30_commands.md b/docs/30_commands.md index 23b997dd..9a774bd7 100644 --- a/docs/30_commands.md +++ b/docs/30_commands.md @@ -211,95 +211,15 @@ Available flags for the command: - `--company-id`, to set the ID of the desired Company - `--project-id`, to set the ID of the desired Project -### pod list +### list RESOURCE-TYPE -The `runtime pod list` subcommand allows you to see all pods that are running for the environment associated to a -given project. +The `runtime list` subcommand allows you to list all resources of a specific type that are running for the +environment associated to a given Project. Usage: ```sh -miactl runtime pod list ENVIRONMENT [flags] -``` - -Available flags for the command: - -- `--endpoint`, to set the Console endpoint (default is `https://console.cloud.mia-platform.eu`) -- `--certificate-authority`, to provide the path to a custom CA certificate -- `--insecure-skip-tls-verify`, to disallow the check the validity of the certificate of the remote endpoint -- `--context`, to specify a different context from the currently selected one -- `--company-id`, to set the ID of the desired Company -- `--project-id`, to set the ID of the desired Project - -### cronjob list - -The `runtime cronjob list` subcommand allows you to see all cronjobs that are running for the environment associated -to a given Project. - -Usage: - -```sh -miactl runtime cronjob list ENVIRONMENT [flags] -``` - -Available flags for the command: - -- `--endpoint`, to set the Console endpoint (default is `https://console.cloud.mia-platform.eu`) -- `--certificate-authority`, to provide the path to a custom CA certificate -- `--insecure-skip-tls-verify`, to disallow the check the validity of the certificate of the remote endpoint -- `--context`, to specify a different context from the currently selected one -- `--company-id`, to set the ID of the desired Company -- `--project-id`, to set the ID of the desired Project - -### job list - -The `runtime job list` subcommand allows you to see all jobs that are running for the environment associated -to a given project. - -Usage: - -```sh -miactl runtime job list ENVIRONMENT [flags] -``` - -Available flags for the command: - -- `--endpoint`, to set the Console endpoint (default is `https://console.cloud.mia-platform.eu`) -- `--certificate-authority`, to provide the path to a custom CA certificate -- `--insecure-skip-tls-verify`, to disallow the check the validity of the certificate of the remote endpoint -- `--context`, to specify a different context from the currently selected one -- `--company-id`, to set the ID of the desired Company -- `--project-id`, to set the ID of the desired Project - -### deployment list - -The `runtime deployment list` subcommand allows you to see all deployments that are running for the environment -associated to a given Project. - -Usage: - -```sh -miactl runtime deployment list ENVIRONMENT [flags] -``` - -Available flags for the command: - -- `--endpoint`, to set the Console endpoint (default is `https://console.cloud.mia-platform.eu`) -- `--certificate-authority`, to provide the path to a custom CA certificate -- `--insecure-skip-tls-verify`, to disallow the check the validity of the certificate of the remote endpoint -- `--context`, to specify a different context from the currently selected one -- `--company-id`, to set the ID of the desired Company -- `--project-id`, to set the ID of the desired Project - -### service list - -The `runtime service list` subcommand allows you to see all services that are running for the environment associated -to a given Project. - -Usage: - -```sh -miactl runtime service list ENVIRONMENT [flags] +miactl runtime list RESOURCE-TYPE ENVIRONMENT [flags] ``` Available flags for the command: diff --git a/go.mod b/go.mod index 6facc2ae..792559cb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/mia-platform/miactl -go 1.21 +go 1.21.1 require ( dario.cat/mergo v1.0.0 diff --git a/internal/cmd/cronjobs/cronjobs.go b/internal/cmd/cronjobs/cronjobs.go deleted file mode 100644 index a4b842df..00000000 --- a/internal/cmd/cronjobs/cronjobs.go +++ /dev/null @@ -1,131 +0,0 @@ -// 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 cronjobs - -import ( - "context" - "fmt" - "os" - "strconv" - "time" - - "github.com/mia-platform/miactl/internal/client" - "github.com/mia-platform/miactl/internal/clioptions" - "github.com/mia-platform/miactl/internal/resources" - "github.com/mia-platform/miactl/internal/util" - "github.com/olekukonko/tablewriter" - "github.com/spf13/cobra" -) - -const ( - listEndpointTemplate = "/api/projects/%s/environments/%s/cronjobs/describe/" -) - -func Command(o *clioptions.CLIOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "cronjob", - Short: "Manage Mia-Platform Console project runtime cronjob resources", - Long: `Manage Mia-Platform Console project runtime cronjob resources. - -A project on Mia-Platform Console once deployed can have one or more cronjob resources associcated with one or more -of its environments. -`, - } - - // add sub commands - cmd.AddCommand( - listCmd(o), - ) - - return cmd -} - -func listCmd(o *clioptions.CLIOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "list ENVIRONMENT", - Short: "List all cronjobs for a project in an environment", - Long: "List all cronjobs for a project in an environment.", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - restConfig, err := o.ToRESTConfig() - cobra.CheckErr(err) - client, err := client.APIClientForConfig(restConfig) - cobra.CheckErr(err) - return printCronJobsList(client, restConfig.ProjectID, args[0]) - }, - } - - return cmd -} - -func printCronJobsList(client *client.APIClient, projectID, environment string) error { - if projectID == "" { - return fmt.Errorf("missing project id, please set one with the flag or context") - } - resp, err := client. - Get(). - APIPath(fmt.Sprintf(listEndpointTemplate, projectID, environment)). - Do(context.Background()) - - if err != nil { - return err - } - - if err := resp.Error(); err != nil { - return err - } - - cronjobs := make([]resources.CronJob, 0) - err = resp.ParseResponse(&cronjobs) - if err != nil { - return err - } - - if len(cronjobs) == 0 { - fmt.Printf("No cronjobs found for %s environment\n", environment) - return nil - } - - table := tablewriter.NewWriter(os.Stdout) - table.SetBorders(tablewriter.Border{Left: false, Top: false, Right: false, Bottom: false}) - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - table.SetRowSeparator("") - table.SetHeader([]string{"Name", "Schedule", "Suspend", "Active", "Last Schedule", "Age"}) - - if err != nil { - return err - } - - for _, cronjob := range cronjobs { - table.Append(rowForCronJob(cronjob)) - } - - table.Render() - return nil -} - -func rowForCronJob(cronjob resources.CronJob) []string { - return []string{ - cronjob.Name, - cronjob.Schedule, - strconv.FormatBool(cronjob.Suspend), - fmt.Sprint(cronjob.Active), - util.HumanDuration(time.Since(cronjob.LastSchedule)), - util.HumanDuration(time.Since(cronjob.Age)), - } -} diff --git a/internal/cmd/cronjobs/cronjobs_test.go b/internal/cmd/cronjobs/cronjobs_test.go deleted file mode 100644 index a5a4ffc2..00000000 --- a/internal/cmd/cronjobs/cronjobs_test.go +++ /dev/null @@ -1,125 +0,0 @@ -// 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 cronjobs - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/mia-platform/miactl/internal/client" - "github.com/mia-platform/miactl/internal/resources" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPrintCronJobsList(t *testing.T) { - testCases := map[string]struct { - testServer *httptest.Server - projectID string - err bool - }{ - "list cronjob with success": { - testServer: testServer(t), - projectID: "found", - }, - "list cronjob with empty response": { - testServer: testServer(t), - projectID: "empty", - }, - "failed request": { - testServer: testServer(t), - projectID: "fail", - err: true, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - server := testCase.testServer - defer server.Close() - - client, err := client.APIClientForConfig(&client.Config{ - Host: server.URL, - }) - require.NoError(t, err) - - err = printCronJobsList(client, testCase.projectID, "env-id") - if testCase.err { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestRowForCronJob(t *testing.T) { - testCases := map[string]struct { - cronjob resources.CronJob - expectedRow []string - }{ - "basic cronjob": { - cronjob: resources.CronJob{ - Name: "cronjob-name", - Suspend: true, - Active: 0, - Schedule: "* * * * *", - Age: time.Now().Add(-time.Hour * 24), - LastSchedule: time.Now(), - }, - expectedRow: []string{"cronjob-name", "* * * * *", "true", "0", "0s", "24h"}, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - assert.Equal(t, testCase.expectedRow, rowForCronJob(testCase.cronjob)) - }) - } -} - -func testServer(t *testing.T) *httptest.Server { - t.Helper() - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "found", "env-id"): - cronjob := resources.CronJob{ - Name: "cronjob-name", - Suspend: true, - Active: 0, - Schedule: "* * * * *", - Age: time.Now().Add(-time.Hour * 24), - LastSchedule: time.Now(), - } - data, err := resources.EncodeResourceToJSON([]resources.CronJob{cronjob}) - require.NoError(t, err) - w.WriteHeader(http.StatusOK) - w.Write(data) - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "fail", "env-id"): - w.WriteHeader(http.StatusNotFound) - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "empty", "env-id"): - w.WriteHeader(http.StatusOK) - w.Write([]byte("[]")) - default: - w.WriteHeader(http.StatusNotFound) - assert.Failf(t, "unexpected http call", "received call with method: %s uri %s", r.Method, r.RequestURI) - } - })) - return server -} diff --git a/internal/cmd/cronjobs/doc.go b/internal/cmd/cronjobs/doc.go deleted file mode 100644 index 92b77473..00000000 --- a/internal/cmd/cronjobs/doc.go +++ /dev/null @@ -1,17 +0,0 @@ -// 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. - -// cronjobs package contains subcommands and functions for managing cronjobs -package cronjobs diff --git a/internal/cmd/deployments/deployments.go b/internal/cmd/deployments/deployments.go deleted file mode 100644 index a470fe24..00000000 --- a/internal/cmd/deployments/deployments.go +++ /dev/null @@ -1,129 +0,0 @@ -// 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 deployments - -import ( - "context" - "fmt" - "os" - "time" - - "github.com/mia-platform/miactl/internal/client" - "github.com/mia-platform/miactl/internal/clioptions" - "github.com/mia-platform/miactl/internal/resources" - "github.com/mia-platform/miactl/internal/util" - "github.com/olekukonko/tablewriter" - "github.com/spf13/cobra" -) - -const ( - listEndpointTemplate = "/api/projects/%s/environments/%s/deployments/describe/" -) - -func Command(o *clioptions.CLIOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "deployment", - Short: "Manage Mia-Platform Console project runtime deployment resources", - Long: `Manage Mia-Platform Console project runtime deployment resources. - -A project on Mia-Platform Console once deployed can have one or more deployment resources associcated with one or more -of its environments. -`, - } - - // add sub commands - cmd.AddCommand( - listCmd(o), - ) - - return cmd -} - -func listCmd(o *clioptions.CLIOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "list ENVIRONMENT", - Short: "List all deployments for a project in an environment", - Long: "List all deployments for a project in an environment.", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - restConfig, err := o.ToRESTConfig() - cobra.CheckErr(err) - client, err := client.APIClientForConfig(restConfig) - cobra.CheckErr(err) - return printDeploymentsList(client, restConfig.ProjectID, args[0]) - }, - } - - return cmd -} - -func printDeploymentsList(client *client.APIClient, projectID, environment string) error { - if projectID == "" { - return fmt.Errorf("missing project id, please set one with the flag or context") - } - resp, err := client. - Get(). - APIPath(fmt.Sprintf(listEndpointTemplate, projectID, environment)). - Do(context.Background()) - - if err != nil { - return err - } - - if err := resp.Error(); err != nil { - return err - } - - deployments := make([]resources.Deployment, 0) - err = resp.ParseResponse(&deployments) - if err != nil { - return err - } - - if len(deployments) == 0 { - fmt.Printf("No deployments found for %s environment\n", environment) - return nil - } - - table := tablewriter.NewWriter(os.Stdout) - table.SetBorders(tablewriter.Border{Left: false, Top: false, Right: false, Bottom: false}) - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - table.SetRowSeparator("") - table.SetHeader([]string{"Name", "Ready", "Up-to-Date", "Available", "Age"}) - - if err != nil { - return err - } - - for _, deployment := range deployments { - table.Append(rowForDeployment(deployment)) - } - - table.Render() - return nil -} - -func rowForDeployment(deployment resources.Deployment) []string { - return []string{ - deployment.Name, - fmt.Sprintf("%d/%d", deployment.Ready, deployment.Available), - fmt.Sprint(deployment.Replicas), - fmt.Sprint(deployment.Available), - util.HumanDuration(time.Since(deployment.Age)), - } -} diff --git a/internal/cmd/deployments/deployments_test.go b/internal/cmd/deployments/deployments_test.go deleted file mode 100644 index 9cadc773..00000000 --- a/internal/cmd/deployments/deployments_test.go +++ /dev/null @@ -1,131 +0,0 @@ -// 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 deployments - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/mia-platform/miactl/internal/client" - "github.com/mia-platform/miactl/internal/resources" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPrintDeploymentsList(t *testing.T) { - testCases := map[string]struct { - testServer *httptest.Server - projectID string - err bool - }{ - "list deployment with success": { - testServer: testServer(t), - projectID: "found", - }, - "list deployment with empty response": { - testServer: testServer(t), - projectID: "empty", - }, - "failed request": { - testServer: testServer(t), - projectID: "fail", - err: true, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - server := testCase.testServer - defer server.Close() - - client, err := client.APIClientForConfig(&client.Config{ - Host: server.URL, - }) - require.NoError(t, err) - - err = printDeploymentsList(client, testCase.projectID, "env-id") - if testCase.err { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestRowForDeployment(t *testing.T) { - testCases := map[string]struct { - deployment resources.Deployment - expectedRow []string - }{ - "basic deployment": { - deployment: resources.Deployment{ - Name: "deployment-name", - Ready: 1, - Replicas: 1, - Available: 1, - Age: time.Now().Add(-time.Hour * 24), - }, - expectedRow: []string{"deployment-name", "1/1", "1", "1", "24h"}, - }, - "missing ready and available": { - deployment: resources.Deployment{ - Name: "deployment-name", - Replicas: 0, - Age: time.Now().Add(-time.Hour * 24), - }, - expectedRow: []string{"deployment-name", "0/0", "0", "0", "24h"}, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - assert.Equal(t, testCase.expectedRow, rowForDeployment(testCase.deployment)) - }) - } -} - -func testServer(t *testing.T) *httptest.Server { - t.Helper() - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "found", "env-id"): - deployment := resources.Deployment{ - Name: "deployment-name", - Ready: 1, - Replicas: 1, - Available: 1, - Age: time.Now().Add(-time.Hour * 24), - } - data, err := resources.EncodeResourceToJSON([]resources.Deployment{deployment}) - require.NoError(t, err) - w.WriteHeader(http.StatusOK) - w.Write(data) - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "fail", "env-id"): - w.WriteHeader(http.StatusNotFound) - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "empty", "env-id"): - w.WriteHeader(http.StatusOK) - w.Write([]byte("[]")) - default: - w.WriteHeader(http.StatusNotFound) - assert.Failf(t, "unexpected http call", "received call with method: %s uri %s", r.Method, r.RequestURI) - } - })) - return server -} diff --git a/internal/cmd/jobs/doc.go b/internal/cmd/jobs/doc.go deleted file mode 100644 index 1b40c4ae..00000000 --- a/internal/cmd/jobs/doc.go +++ /dev/null @@ -1,17 +0,0 @@ -// 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. - -// jobs package contains subcommands and functions for managing jobs -package jobs diff --git a/internal/cmd/jobs/jobs.go b/internal/cmd/jobs/jobs.go deleted file mode 100644 index a04f495a..00000000 --- a/internal/cmd/jobs/jobs.go +++ /dev/null @@ -1,133 +0,0 @@ -// 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 jobs - -import ( - "context" - "fmt" - "os" - "time" - - "github.com/mia-platform/miactl/internal/client" - "github.com/mia-platform/miactl/internal/clioptions" - "github.com/mia-platform/miactl/internal/resources" - "github.com/mia-platform/miactl/internal/util" - "github.com/olekukonko/tablewriter" - "github.com/spf13/cobra" -) - -const ( - listEndpointTemplate = "/api/projects/%s/environments/%s/jobs/describe/" -) - -func Command(o *clioptions.CLIOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "job", - Short: "Manage Mia-Platform Console project runtime job resources", - Long: `Manage Mia-Platform Console project runtime job resources. - -A project on Mia-Platform Console once deployed can have one or more job resources associcated with one or more -of its environments. -`, - } - - // add sub commands - cmd.AddCommand( - listCmd(o), - ) - - return cmd -} - -func listCmd(o *clioptions.CLIOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "list ENVIRONMENT", - Short: "List all jobs for a project in an environment", - Long: "List all jobs for a project in an environment.", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - restConfig, err := o.ToRESTConfig() - cobra.CheckErr(err) - client, err := client.APIClientForConfig(restConfig) - cobra.CheckErr(err) - return printJobsList(client, restConfig.ProjectID, args[0]) - }, - } - - return cmd -} - -func printJobsList(client *client.APIClient, projectID, environment string) error { - if projectID == "" { - return fmt.Errorf("missing project id, please set one with the flag or context") - } - resp, err := client. - Get(). - APIPath(fmt.Sprintf(listEndpointTemplate, projectID, environment)). - Do(context.Background()) - - if err != nil { - return err - } - - if err := resp.Error(); err != nil { - return err - } - - jobs := make([]resources.Job, 0) - err = resp.ParseResponse(&jobs) - if err != nil { - return err - } - - if len(jobs) == 0 { - fmt.Printf("No jobs found for %s environment\n", environment) - return nil - } - - table := tablewriter.NewWriter(os.Stdout) - table.SetBorders(tablewriter.Border{Left: false, Top: false, Right: false, Bottom: false}) - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - table.SetRowSeparator("") - table.SetHeader([]string{"Name", "Finished Pods", "Duration", "Age"}) - - if err != nil { - return err - } - - for _, job := range jobs { - table.Append(rowForJob(job)) - } - - table.Render() - return nil -} - -func rowForJob(job resources.Job) []string { - duration := "-" - if !job.CompletionTime.IsZero() { - duration = util.HumanDuration(job.CompletionTime.Sub(job.StartTime)) - } - - return []string{ - job.Name, - fmt.Sprintf("%d/%d", job.Succeeded, (job.Active + job.Failed + job.Succeeded)), - duration, - util.HumanDuration(time.Since(job.Age)), - } -} diff --git a/internal/cmd/jobs/jobs_test.go b/internal/cmd/jobs/jobs_test.go deleted file mode 100644 index 73a0b400..00000000 --- a/internal/cmd/jobs/jobs_test.go +++ /dev/null @@ -1,136 +0,0 @@ -// 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 jobs - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/mia-platform/miactl/internal/client" - "github.com/mia-platform/miactl/internal/resources" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPrintJobsList(t *testing.T) { - testCases := map[string]struct { - testServer *httptest.Server - projectID string - err bool - }{ - "list job with success": { - testServer: testServer(t), - projectID: "found", - }, - "list job with empty response": { - testServer: testServer(t), - projectID: "empty", - }, - "failed request": { - testServer: testServer(t), - projectID: "fail", - err: true, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - server := testCase.testServer - defer server.Close() - - client, err := client.APIClientForConfig(&client.Config{ - Host: server.URL, - }) - require.NoError(t, err) - - err = printJobsList(client, testCase.projectID, "env-id") - if testCase.err { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestRowForJob(t *testing.T) { - testCases := map[string]struct { - job resources.Job - expectedRow []string - }{ - "basic job": { - job: resources.Job{ - Name: "job-name", - Active: 0, - Succeeded: 1, - Failed: 0, - Age: time.Now().Add(-time.Hour * 24), - StartTime: time.Now().Add(-time.Second * 60), - CompletionTime: time.Now(), - }, - expectedRow: []string{"job-name", "1/1", "60s", "24h"}, - }, - "failed job": { - job: resources.Job{ - Name: "job-name", - Failed: 1, - Age: time.Now().Add(-time.Hour * 24), - StartTime: time.Now().Add(-time.Second * 60), - }, - expectedRow: []string{"job-name", "0/1", "-", "24h"}, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - assert.Equal(t, testCase.expectedRow, rowForJob(testCase.job)) - }) - } -} - -func testServer(t *testing.T) *httptest.Server { - t.Helper() - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "found", "env-id"): - job := resources.Job{ - Name: "job-name", - Active: 0, - Succeeded: 1, - Failed: 0, - Age: time.Now().Add(-time.Hour * 24), - StartTime: time.Now().Add(-time.Second * 60), - CompletionTime: time.Now(), - } - data, err := resources.EncodeResourceToJSON([]resources.Job{job}) - require.NoError(t, err) - w.WriteHeader(http.StatusOK) - w.Write(data) - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "fail", "env-id"): - w.WriteHeader(http.StatusNotFound) - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "empty", "env-id"): - w.WriteHeader(http.StatusOK) - w.Write([]byte("[]")) - default: - w.WriteHeader(http.StatusNotFound) - assert.Failf(t, "unexpected http call", "received call with method: %s uri %s", r.Method, r.RequestURI) - } - })) - return server -} diff --git a/internal/cmd/pods/doc.go b/internal/cmd/pods/doc.go deleted file mode 100644 index a3138eda..00000000 --- a/internal/cmd/pods/doc.go +++ /dev/null @@ -1,17 +0,0 @@ -// 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. - -// pods package contains subcommands and functions for managing pods -package pods diff --git a/internal/cmd/pods/pods.go b/internal/cmd/pods/pods.go deleted file mode 100644 index 779c56f4..00000000 --- a/internal/cmd/pods/pods.go +++ /dev/null @@ -1,163 +0,0 @@ -// 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 pods - -import ( - "context" - "fmt" - "os" - "strings" - "time" - - "github.com/mia-platform/miactl/internal/client" - "github.com/mia-platform/miactl/internal/clioptions" - "github.com/mia-platform/miactl/internal/resources" - "github.com/mia-platform/miactl/internal/util" - "github.com/olekukonko/tablewriter" - "github.com/spf13/cobra" - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -const ( - listEndpointTemplate = "/api/projects/%s/environments/%s/pods/describe/" -) - -func Command(o *clioptions.CLIOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "pod", - Short: "Manage Mia-Platform Console project runtime pod resources", - Long: `Manage Mia-Platform Console project runtime pod resources. - -A project on Mia-Platform Console once deployed can have one or more pod resources associcated with one or more -of its environments. -`, - } - - // add sub commands - cmd.AddCommand( - listCmd(o), - ) - - return cmd -} - -func listCmd(o *clioptions.CLIOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "list ENVIRONMENT", - Short: "List all pods for a project in an environment", - Long: "List all pods for a project in an environment.", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - restConfig, err := o.ToRESTConfig() - cobra.CheckErr(err) - client, err := client.APIClientForConfig(restConfig) - cobra.CheckErr(err) - return printPodsList(client, restConfig.ProjectID, args[0]) - }, - } - - return cmd -} - -func printPodsList(client *client.APIClient, projectID, environment string) error { - if projectID == "" { - return fmt.Errorf("missing project id, please set one with the flag or context") - } - resp, err := client. - Get(). - APIPath(fmt.Sprintf(listEndpointTemplate, projectID, environment)). - Do(context.Background()) - - if err != nil { - return err - } - - if err := resp.Error(); err != nil { - return err - } - - pods := make([]resources.Pod, 0) - err = resp.ParseResponse(&pods) - if err != nil { - return err - } - - if len(pods) == 0 { - fmt.Printf("No pods found for %s environment\n", environment) - return nil - } - - table := tablewriter.NewWriter(os.Stdout) - table.SetBorders(tablewriter.Border{Left: false, Top: false, Right: false, Bottom: false}) - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - table.SetRowSeparator("") - table.SetHeader([]string{"Status", "Name", "Application", "Ready", "Phase", "Restart", "Age"}) - - if err != nil { - return err - } - - for _, pod := range pods { - table.Append(rowForPod(pod)) - } - - table.Render() - return nil -} - -func rowForPod(pod resources.Pod) []string { - totalRestart := 0 - totalContainers := 0 - readyContainers := 0 - for _, container := range pod.Containers { - totalRestart += container.RestartCount - totalContainers++ - if container.Ready { - readyContainers++ - } - } - - components := make([]string, 0) - for _, component := range pod.Component { - if len(component.Name) == 0 { - continue - } - - nameComponents := []string{component.Name} - if len(component.Version) > 0 { - nameComponents = append(nameComponents, component.Version) - } - components = append(components, strings.Join(nameComponents, ":")) - } - - if len(components) == 0 { - components = append(components, "-") - } - - caser := cases.Title(language.English) - return []string{ - caser.String(pod.Status), - pod.Name, - strings.Join(components, ", "), - fmt.Sprintf("%d/%d", readyContainers, totalContainers), - caser.String(pod.Phase), - fmt.Sprint(totalRestart), - util.HumanDuration(time.Since(pod.Age)), - } -} diff --git a/internal/cmd/deployments/doc.go b/internal/cmd/resources/doc.go similarity index 85% rename from internal/cmd/deployments/doc.go rename to internal/cmd/resources/doc.go index 9b4fa0c1..22e2ed3e 100644 --- a/internal/cmd/deployments/doc.go +++ b/internal/cmd/resources/doc.go @@ -13,5 +13,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -// deployments package contains subcommands and functions for managing deployments -package deployments +// resources package contains subcommands and functions for managing runtime resources +package resources diff --git a/internal/cmd/resources/parsers.go b/internal/cmd/resources/parsers.go new file mode 100644 index 00000000..b9056bef --- /dev/null +++ b/internal/cmd/resources/parsers.go @@ -0,0 +1,138 @@ +// 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 resources + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/mia-platform/miactl/internal/client" + "github.com/mia-platform/miactl/internal/resources" + "github.com/mia-platform/miactl/internal/util" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +func rowsForResources[T any](response *client.Response, rowParser func(T) []string) ([][]string, error) { + resources := make([]T, 0) + if err := response.ParseResponse(&resources); err != nil { + return nil, err + } + + rows := make([][]string, 0) + for _, resource := range resources { + rows = append(rows, rowParser(resource)) + } + return rows, nil +} + +func rowForService(service resources.Service) []string { + ports := make([]string, 0) + for _, port := range service.Ports { + ports = append(ports, fmt.Sprintf("%d/%s", port.Port, port.Protocol)) + } + + clusterIP := service.ClusterIP + if len(clusterIP) == 0 { + clusterIP = "" + } + + return []string{ + service.Name, + service.Type, + clusterIP, + strings.Join(ports, ","), + util.HumanDuration(time.Since(service.Age)), + } +} + +func rowForPod(pod resources.Pod) []string { + totalRestart := 0 + totalContainers := 0 + readyContainers := 0 + for _, container := range pod.Containers { + totalRestart += container.RestartCount + totalContainers++ + if container.Ready { + readyContainers++ + } + } + + components := make([]string, 0) + for _, component := range pod.Component { + if len(component.Name) == 0 { + continue + } + + nameComponents := []string{component.Name} + if len(component.Version) > 0 { + nameComponents = append(nameComponents, component.Version) + } + components = append(components, strings.Join(nameComponents, ":")) + } + + if len(components) == 0 { + components = append(components, "-") + } + + caser := cases.Title(language.English) + return []string{ + caser.String(pod.Status), + pod.Name, + strings.Join(components, ", "), + fmt.Sprintf("%d/%d", readyContainers, totalContainers), + caser.String(pod.Phase), + fmt.Sprint(totalRestart), + util.HumanDuration(time.Since(pod.Age)), + } +} + +func rowForJob(job resources.Job) []string { + duration := "-" + if !job.CompletionTime.IsZero() { + duration = util.HumanDuration(job.CompletionTime.Sub(job.StartTime)) + } + + return []string{ + job.Name, + fmt.Sprintf("%d/%d", job.Succeeded, (job.Active + job.Failed + job.Succeeded)), + duration, + util.HumanDuration(time.Since(job.Age)), + } +} + +func rowForDeployment(deployment resources.Deployment) []string { + return []string{ + deployment.Name, + fmt.Sprintf("%d/%d", deployment.Ready, deployment.Available), + fmt.Sprint(deployment.Replicas), + fmt.Sprint(deployment.Available), + util.HumanDuration(time.Since(deployment.Age)), + } +} + +func rowForCronJob(cronjob resources.CronJob) []string { + return []string{ + cronjob.Name, + cronjob.Schedule, + strconv.FormatBool(cronjob.Suspend), + fmt.Sprint(cronjob.Active), + util.HumanDuration(time.Since(cronjob.LastSchedule)), + util.HumanDuration(time.Since(cronjob.Age)), + } +} diff --git a/internal/cmd/pods/pods_test.go b/internal/cmd/resources/parsers_test.go similarity index 52% rename from internal/cmd/pods/pods_test.go rename to internal/cmd/resources/parsers_test.go index 2e5e15cb..c2a7f9a4 100644 --- a/internal/cmd/pods/pods_test.go +++ b/internal/cmd/resources/parsers_test.go @@ -13,58 +13,104 @@ // See the License for the specific language governing permissions and // limitations under the License. -package pods +package resources import ( - "fmt" - "net/http" - "net/http/httptest" "testing" "time" - "github.com/mia-platform/miactl/internal/client" "github.com/mia-platform/miactl/internal/resources" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestPrintPodsList(t *testing.T) { +func TestRowForCronJob(t *testing.T) { testCases := map[string]struct { - testServer *httptest.Server - projectID string - err bool + cronjob resources.CronJob + expectedRow []string }{ - "list pod with success": { - testServer: testServer(t), - projectID: "found", + "basic cronjob": { + cronjob: resources.CronJob{ + Name: "cronjob-name", + Suspend: true, + Active: 0, + Schedule: "* * * * *", + Age: time.Now().Add(-time.Hour * 24), + LastSchedule: time.Now(), + }, + expectedRow: []string{"cronjob-name", "* * * * *", "true", "0", "0s", "24h"}, }, - "list pod with empty response": { - testServer: testServer(t), - projectID: "empty", + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + assert.Equal(t, testCase.expectedRow, rowForCronJob(testCase.cronjob)) + }) + } +} + +func TestRowForDeployment(t *testing.T) { + testCases := map[string]struct { + deployment resources.Deployment + expectedRow []string + }{ + "basic deployment": { + deployment: resources.Deployment{ + Name: "deployment-name", + Ready: 1, + Replicas: 1, + Available: 1, + Age: time.Now().Add(-time.Hour * 24), + }, + expectedRow: []string{"deployment-name", "1/1", "1", "1", "24h"}, }, - "failed request": { - testServer: testServer(t), - projectID: "fail", - err: true, + "missing ready and available": { + deployment: resources.Deployment{ + Name: "deployment-name", + Replicas: 0, + Age: time.Now().Add(-time.Hour * 24), + }, + expectedRow: []string{"deployment-name", "0/0", "0", "0", "24h"}, }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { - server := testCase.testServer - defer server.Close() + assert.Equal(t, testCase.expectedRow, rowForDeployment(testCase.deployment)) + }) + } +} - client, err := client.APIClientForConfig(&client.Config{ - Host: server.URL, - }) - require.NoError(t, err) +func TestRowForJob(t *testing.T) { + testCases := map[string]struct { + job resources.Job + expectedRow []string + }{ + "basic job": { + job: resources.Job{ + Name: "job-name", + Active: 0, + Succeeded: 1, + Failed: 0, + Age: time.Now().Add(-time.Hour * 24), + StartTime: time.Now().Add(-time.Second * 60), + CompletionTime: time.Now(), + }, + expectedRow: []string{"job-name", "1/1", "60s", "24h"}, + }, + "failed job": { + job: resources.Job{ + Name: "job-name", + Failed: 1, + Age: time.Now().Add(-time.Hour * 24), + StartTime: time.Now().Add(-time.Second * 60), + }, + expectedRow: []string{"job-name", "0/1", "-", "24h"}, + }, + } - err = printPodsList(client, testCase.projectID, "env-id") - if testCase.err { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + assert.Equal(t, testCase.expectedRow, rowForJob(testCase.job)) }) } } @@ -179,49 +225,49 @@ func TestRowForPod(t *testing.T) { } } -func testServer(t *testing.T) *httptest.Server { - t.Helper() - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "found", "env-id"): - pod := resources.Pod{ - Name: "pod-name", - Phase: "running", - Status: "ok", - Age: time.Now(), - Component: []struct { - Name string `json:"name"` - Version string `json:"version"` - }{ - {Name: "component", Version: "version"}, +func TestRowForService(t *testing.T) { + testCases := map[string]struct { + service resources.Service + expectedRow []string + }{ + "basic service": { + service: resources.Service{ + Name: "service-name", + Type: "ClusterIP", + ClusterIP: "127.0.0.1", + Ports: []resources.Port{ + { + Name: "port-name", + Port: 8000, + Protocol: "TCP", + TargetPort: "8000", + }, }, - Containers: []struct { - Name string `json:"name"` - Ready bool `json:"ready"` - RestartCount int `json:"restartCount"` - Status string `json:"status"` - }{ + Age: time.Now().Add(-time.Hour * 24), + }, + expectedRow: []string{"service-name", "ClusterIP", "127.0.0.1", "8000/TCP", "24h"}, + }, + "missing cluster ip": { + service: resources.Service{ + Name: "service-name", + Type: "ClusterIP", + Ports: []resources.Port{ { - Name: "container-name", - Ready: true, - RestartCount: 0, - Status: "running", + Name: "port-name", + Port: 8000, + Protocol: "TCP", + TargetPort: "8000", }, }, - } - data, err := resources.EncodeResourceToJSON([]resources.Pod{pod}) - require.NoError(t, err) - w.WriteHeader(http.StatusOK) - w.Write(data) - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "fail", "env-id"): - w.WriteHeader(http.StatusNotFound) - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "empty", "env-id"): - w.WriteHeader(http.StatusOK) - w.Write([]byte("[]")) - default: - w.WriteHeader(http.StatusNotFound) - assert.Failf(t, "unexpected http call", "received call with method: %s uri %s", r.Method, r.RequestURI) - } - })) - return server + Age: time.Now().Add(-time.Hour * 24), + }, + expectedRow: []string{"service-name", "ClusterIP", "", "8000/TCP", "24h"}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + assert.Equal(t, testCase.expectedRow, rowForService(testCase.service)) + }) + } } diff --git a/internal/cmd/resources/resources.go b/internal/cmd/resources/resources.go new file mode 100644 index 00000000..944ee377 --- /dev/null +++ b/internal/cmd/resources/resources.go @@ -0,0 +1,174 @@ +// 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 resources + +import ( + "context" + "fmt" + "os" + "slices" + "strings" + + "github.com/mia-platform/miactl/internal/client" + "github.com/mia-platform/miactl/internal/clioptions" + "github.com/mia-platform/miactl/internal/resources" + "github.com/olekukonko/tablewriter" + "github.com/spf13/cobra" +) + +const ( + CronJobResourceType = "cronjob" + CronJobsResourceType = "cronjobs" + + DeploymentResourceType = "deployment" + DeploymentsResourceType = "deployments" + + JobResourceType = "job" + JobsResourceType = "jobs" + + PodResourceType = "pod" + PodsResourceType = "pods" + + ServiceResourceType = "service" + ServicesResourceType = "services" + + listEndpointTemplate = "/api/projects/%s/environments/%s/%s/describe/" +) + +var resourcesAvailable = []string{ + CronJobResourceType, + CronJobsResourceType, + DeploymentResourceType, + DeploymentsResourceType, + JobResourceType, + JobsResourceType, + PodResourceType, + PodsResourceType, + ServiceResourceType, + ServicesResourceType, +} + +func ListCommand(o *clioptions.CLIOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "list RESOURCE-TYPE ENVIRONMENT", + Short: "List Mia-Platform Console runtime resources", + Long: `List Mia-Platform Console runtime resources. + +A project on Mia-Platform Console once deployed can have one or more resource of different kinds associcated with one +or more of its environments.`, + Args: cobra.ExactArgs(2), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return resourcesCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp + }, + RunE: func(cmd *cobra.Command, args []string) error { + restConfig, err := o.ToRESTConfig() + cobra.CheckErr(err) + client, err := client.APIClientForConfig(restConfig) + cobra.CheckErr(err) + return printList(client, restConfig.ProjectID, args[0], args[1]) + }, + } + + return cmd +} + +func resourcesCompletions(args []string, toComplete string) []string { + resources := make([]string, 0) + if len(args) > 0 { + return resources + } + + for _, resource := range resourcesAvailable { + if strings.HasPrefix(resource, toComplete) { + resources = append(resources, resource) + } + } + + return resources +} + +func printList(client *client.APIClient, projectID, resourceType, environment string) error { + if projectID == "" { + return fmt.Errorf("missing project id, please set one with the flag or context") + } + + if !slices.Contains(resourcesAvailable, resourceType) { + return fmt.Errorf("unknown resource type: %s", resourceType) + } + + if !strings.HasSuffix(resourceType, "s") { + resourceType += "s" + } + + resp, err := client. + Get(). + APIPath(fmt.Sprintf(listEndpointTemplate, projectID, environment, resourceType)). + Do(context.Background()) + + if err != nil { + return err + } + + if err := resp.Error(); err != nil { + return err + } + + tableHeaders := make([]string, 0) + canonicalType := "" + var rows [][]string + switch resourceType { + case PodResourceType, PodsResourceType: + tableHeaders = append(tableHeaders, "Status", "Name", "Application", "Ready", "Phase", "Restart", "Age") + rows, err = rowsForResources[resources.Pod](resp, rowForPod) + canonicalType = PodsResourceType + case CronJobResourceType, CronJobsResourceType: + tableHeaders = append(tableHeaders, "Name", "Schedule", "Suspend", "Active", "Last Schedule", "Age") + rows, err = rowsForResources[resources.CronJob](resp, rowForCronJob) + canonicalType = CronJobsResourceType + case DeploymentResourceType, DeploymentsResourceType: + tableHeaders = append(tableHeaders, "Name", "Ready", "Up-to-Date", "Available", "Age") + rows, err = rowsForResources[resources.Deployment](resp, rowForDeployment) + canonicalType = DeploymentsResourceType + case JobResourceType, JobsResourceType: + tableHeaders = append(tableHeaders, "Name", "Finished Pods", "Duration", "Age") + rows, err = rowsForResources[resources.Job](resp, rowForJob) + canonicalType = JobsResourceType + case ServiceResourceType, ServicesResourceType: + tableHeaders = append(tableHeaders, "Name", "Type", "Cluster-IP", "Port(s)", "Age") + rows, err = rowsForResources[resources.Service](resp, rowForService) + canonicalType = ServicesResourceType + } + if err != nil { + return err + } + + if len(rows) == 0 { + fmt.Printf("No %s found for %s environment\n", canonicalType, environment) + return nil + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetBorders(tablewriter.Border{Left: false, Top: false, Right: false, Bottom: false}) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetAutoWrapText(false) + table.SetHeader(tableHeaders) + table.AppendBulk(rows) + table.Render() + return nil +} diff --git a/internal/cmd/resources/resources_test.go b/internal/cmd/resources/resources_test.go new file mode 100644 index 00000000..c8ea8260 --- /dev/null +++ b/internal/cmd/resources/resources_test.go @@ -0,0 +1,199 @@ +// 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 resources + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/mia-platform/miactl/internal/client" + "github.com/mia-platform/miactl/internal/resources" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPrintServicesList(t *testing.T) { + testCases := map[string]struct { + testServer *httptest.Server + resourceType string + projectID string + err bool + }{ + "list services with success": { + testServer: testServer(t), + projectID: "found", + resourceType: ServicesResourceType, + }, + "list deployments with success": { + testServer: testServer(t), + projectID: "found", + resourceType: DeploymentsResourceType, + }, + "list pods with success": { + testServer: testServer(t), + projectID: "found", + resourceType: PodsResourceType, + }, + "list cronjobs with success": { + testServer: testServer(t), + projectID: "found", + resourceType: CronJobsResourceType, + }, + "list jobs with success": { + testServer: testServer(t), + projectID: "found", + resourceType: JobsResourceType, + }, + "list deployments with empty response": { + testServer: testServer(t), + projectID: "empty", + resourceType: DeploymentResourceType, + }, + "failed request": { + testServer: testServer(t), + projectID: "fail", + err: true, + resourceType: PodsResourceType, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + server := testCase.testServer + defer server.Close() + + client, err := client.APIClientForConfig(&client.Config{ + Host: server.URL, + }) + require.NoError(t, err) + + err = printList(client, testCase.projectID, testCase.resourceType, "env-id") + if testCase.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func testServer(t *testing.T) *httptest.Server { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "found", "env-id", ServicesResourceType): + resource := resources.Service{ + Name: "service-name", + Type: "ClusterIP", + ClusterIP: "127.0.0.1", + Ports: []resources.Port{ + { + Name: "port-name", + Port: 8000, + Protocol: "TCP", + TargetPort: "8000", + }, + }, + Age: time.Now().Add(-time.Hour * 24), + } + data, err := resources.EncodeResourceToJSON([]resources.Service{resource}) + require.NoError(t, err) + w.WriteHeader(http.StatusOK) + w.Write(data) + case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "found", "env-id", DeploymentsResourceType): + resource := resources.Deployment{ + Name: "deployment-name", + Ready: 1, + Replicas: 1, + Available: 1, + Age: time.Now().Add(-time.Hour * 24), + } + data, err := resources.EncodeResourceToJSON([]resources.Deployment{resource}) + require.NoError(t, err) + w.WriteHeader(http.StatusOK) + w.Write(data) + case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "found", "env-id", PodsResourceType): + resource := resources.Pod{ + Name: "pod-name", + Phase: "running", + Status: "ok", + Age: time.Now(), + Component: []struct { + Name string `json:"name"` + Version string `json:"version"` + }{ + {Name: "component", Version: "version"}, + }, + Containers: []struct { + Name string `json:"name"` + Ready bool `json:"ready"` + RestartCount int `json:"restartCount"` + Status string `json:"status"` + }{ + { + Name: "container-name", + Ready: true, + RestartCount: 0, + Status: "running", + }, + }, + } + data, err := resources.EncodeResourceToJSON([]resources.Pod{resource}) + require.NoError(t, err) + w.WriteHeader(http.StatusOK) + w.Write(data) + case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "found", "env-id", CronJobsResourceType): + resource := resources.CronJob{ + Name: "cronjob-name", + Suspend: true, + Active: 0, + Schedule: "* * * * *", + Age: time.Now().Add(-time.Hour * 24), + LastSchedule: time.Now(), + } + data, err := resources.EncodeResourceToJSON([]resources.CronJob{resource}) + require.NoError(t, err) + w.WriteHeader(http.StatusOK) + w.Write(data) + case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "found", "env-id", JobsResourceType): + resource := resources.Job{ + Name: "job-name", + Active: 0, + Succeeded: 1, + Failed: 0, + Age: time.Now().Add(-time.Hour * 24), + StartTime: time.Now().Add(-time.Second * 60), + CompletionTime: time.Now(), + } + data, err := resources.EncodeResourceToJSON([]resources.Job{resource}) + require.NoError(t, err) + w.WriteHeader(http.StatusOK) + w.Write(data) + case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "empty", "env-id", DeploymentsResourceType): + w.WriteHeader(http.StatusOK) + w.Write([]byte("[]")) + case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "fail", "env-id", PodsResourceType): + w.WriteHeader(http.StatusNotFound) + default: + w.WriteHeader(http.StatusNotFound) + assert.Failf(t, "unexpected http call", "received call with method: %s uri %s", r.Method, r.RequestURI) + } + })) + return server +} diff --git a/internal/cmd/runtime.go b/internal/cmd/runtime.go index d7f88d6e..15ca627e 100644 --- a/internal/cmd/runtime.go +++ b/internal/cmd/runtime.go @@ -17,13 +17,9 @@ package cmd import ( "github.com/mia-platform/miactl/internal/clioptions" - "github.com/mia-platform/miactl/internal/cmd/cronjobs" - "github.com/mia-platform/miactl/internal/cmd/deployments" "github.com/mia-platform/miactl/internal/cmd/environments" "github.com/mia-platform/miactl/internal/cmd/events" - "github.com/mia-platform/miactl/internal/cmd/jobs" - "github.com/mia-platform/miactl/internal/cmd/pods" - "github.com/mia-platform/miactl/internal/cmd/services" + runtimeresources "github.com/mia-platform/miactl/internal/cmd/resources" "github.com/spf13/cobra" ) @@ -47,12 +43,8 @@ the resources generated, like Pods, Cronjobs and logs. // add sub commands cmd.AddCommand( + runtimeresources.ListCommand(o), environments.EnvironmentCmd(o), - pods.Command(o), - cronjobs.Command(o), - jobs.Command(o), - deployments.Command(o), - services.Command(o), events.Command(o), ) diff --git a/internal/cmd/services/doc.go b/internal/cmd/services/doc.go deleted file mode 100644 index aafdd01a..00000000 --- a/internal/cmd/services/doc.go +++ /dev/null @@ -1,17 +0,0 @@ -// 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. - -// services package contains subcommands and functions for managing services -package services diff --git a/internal/cmd/services/services.go b/internal/cmd/services/services.go deleted file mode 100644 index 8a6c7749..00000000 --- a/internal/cmd/services/services.go +++ /dev/null @@ -1,140 +0,0 @@ -// 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 services - -import ( - "context" - "fmt" - "os" - "strings" - "time" - - "github.com/mia-platform/miactl/internal/client" - "github.com/mia-platform/miactl/internal/clioptions" - "github.com/mia-platform/miactl/internal/resources" - "github.com/mia-platform/miactl/internal/util" - "github.com/olekukonko/tablewriter" - "github.com/spf13/cobra" -) - -const ( - listEndpointTemplate = "/api/projects/%s/environments/%s/services/describe/" -) - -func Command(o *clioptions.CLIOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "service", - Short: "Manage Mia-Platform Console project runtime service resources", - Long: `Manage Mia-Platform Console project runtime service resources. - -A project on Mia-Platform Console once deployed can have one or more service resources associcated with one or more -of its environments. -`, - } - - // add sub commands - cmd.AddCommand( - listCmd(o), - ) - - return cmd -} - -func listCmd(o *clioptions.CLIOptions) *cobra.Command { - cmd := &cobra.Command{ - Use: "list ENVIRONMENT", - Short: "List all services for a project in an environment", - Long: "List all services for a project in an environment.", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - restConfig, err := o.ToRESTConfig() - cobra.CheckErr(err) - client, err := client.APIClientForConfig(restConfig) - cobra.CheckErr(err) - return printServicesList(client, restConfig.ProjectID, args[0]) - }, - } - - return cmd -} - -func printServicesList(client *client.APIClient, projectID, environment string) error { - if projectID == "" { - return fmt.Errorf("missing project id, please set one with the flag or context") - } - resp, err := client. - Get(). - APIPath(fmt.Sprintf(listEndpointTemplate, projectID, environment)). - Do(context.Background()) - - if err != nil { - return err - } - - if err := resp.Error(); err != nil { - return err - } - - services := make([]resources.Service, 0) - err = resp.ParseResponse(&services) - if err != nil { - return err - } - - if len(services) == 0 { - fmt.Printf("No services found for %s environment\n", environment) - return nil - } - - table := tablewriter.NewWriter(os.Stdout) - table.SetBorders(tablewriter.Border{Left: false, Top: false, Right: false, Bottom: false}) - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - table.SetRowSeparator("") - table.SetHeader([]string{"Name", "Type", "Cluster-IP", "Port(s)", "Age"}) - - if err != nil { - return err - } - - for _, service := range services { - table.Append(rowForService(service)) - } - - table.Render() - return nil -} - -func rowForService(service resources.Service) []string { - ports := make([]string, 0) - for _, port := range service.Ports { - ports = append(ports, fmt.Sprintf("%d/%s", port.Port, port.Protocol)) - } - - clusterIP := service.ClusterIP - if len(clusterIP) == 0 { - clusterIP = "" - } - - return []string{ - service.Name, - service.Type, - clusterIP, - strings.Join(ports, ","), - util.HumanDuration(time.Since(service.Age)), - } -} diff --git a/internal/cmd/services/services_test.go b/internal/cmd/services/services_test.go deleted file mode 100644 index 9a4e42fe..00000000 --- a/internal/cmd/services/services_test.go +++ /dev/null @@ -1,153 +0,0 @@ -// 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 services - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/mia-platform/miactl/internal/client" - "github.com/mia-platform/miactl/internal/resources" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPrintServicesList(t *testing.T) { - testCases := map[string]struct { - testServer *httptest.Server - projectID string - err bool - }{ - "list service with success": { - testServer: testServer(t), - projectID: "found", - }, - "list service with empty response": { - testServer: testServer(t), - projectID: "empty", - }, - "failed request": { - testServer: testServer(t), - projectID: "fail", - err: true, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - server := testCase.testServer - defer server.Close() - - client, err := client.APIClientForConfig(&client.Config{ - Host: server.URL, - }) - require.NoError(t, err) - - err = printServicesList(client, testCase.projectID, "env-id") - if testCase.err { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestRowForService(t *testing.T) { - testCases := map[string]struct { - service resources.Service - expectedRow []string - }{ - "basic service": { - service: resources.Service{ - Name: "service-name", - Type: "ClusterIP", - ClusterIP: "127.0.0.1", - Ports: []resources.Port{ - { - Name: "port-name", - Port: 8000, - Protocol: "TCP", - TargetPort: "8000", - }, - }, - Age: time.Now().Add(-time.Hour * 24), - }, - expectedRow: []string{"service-name", "ClusterIP", "127.0.0.1", "8000/TCP", "24h"}, - }, - "missing cluster ip": { - service: resources.Service{ - Name: "service-name", - Type: "ClusterIP", - Ports: []resources.Port{ - { - Name: "port-name", - Port: 8000, - Protocol: "TCP", - TargetPort: "8000", - }, - }, - Age: time.Now().Add(-time.Hour * 24), - }, - expectedRow: []string{"service-name", "ClusterIP", "", "8000/TCP", "24h"}, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - assert.Equal(t, testCase.expectedRow, rowForService(testCase.service)) - }) - } -} - -func testServer(t *testing.T) *httptest.Server { - t.Helper() - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "found", "env-id"): - service := resources.Service{ - Name: "service-name", - Type: "ClusterIP", - ClusterIP: "127.0.0.1", - Ports: []resources.Port{ - { - Name: "port-name", - Port: 8000, - Protocol: "TCP", - TargetPort: "8000", - }, - }, - Age: time.Now().Add(-time.Hour * 24), - } - data, err := resources.EncodeResourceToJSON([]resources.Service{service}) - require.NoError(t, err) - w.WriteHeader(http.StatusOK) - w.Write(data) - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "fail", "env-id"): - w.WriteHeader(http.StatusNotFound) - case r.Method == http.MethodGet && r.URL.Path == fmt.Sprintf(listEndpointTemplate, "empty", "env-id"): - w.WriteHeader(http.StatusOK) - w.Write([]byte("[]")) - default: - w.WriteHeader(http.StatusNotFound) - assert.Failf(t, "unexpected http call", "received call with method: %s uri %s", r.Method, r.RequestURI) - } - })) - return server -}