diff --git a/.vscode/launch.json b/.vscode/launch.json index e278eaa..0a890b2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,15 +19,19 @@ "request": "launch", "mode": "auto", "program": "${workspaceFolder}/main.go", + "env": { + "APP_SERVICE_ACCOUNT_TOKEN": "token" + }, "args": [ - "--from-config", - "--cluster", - "weekly-47", - "--api-environment", - "qa", + "--token-environment", + "--context", + "playground", "build-deploy", + "--application", + "echo", "-b", - "master" + "master", + "-f" ] }, { @@ -40,22 +44,64 @@ "APP_SERVICE_ACCOUNT_TOKEN": "token" }, "args": [ - "--from-config", "--token-environment", - "--cluster", - "weekly-47", - "--api-environment", - "dev", + "--from-config", "set", "environment-secret", + "--context", + "playground", "-b", "master", "--component", - "auth-proxy", + "api", "-s", - "OAUTH2_PROXY_CLIENT_SECRET", + "APPINSIGHTS_INSTRUMENTATIONKEY", "-v", - "my-pass" + "iknu-test" + ] + }, + { + "name": "Lauch radix-cli follow component", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/main.go", + "env": { + "APP_SERVICE_ACCOUNT_TOKEN": "token" + }, + "args": [ + "--token-environment", + "follow", + "component", + "--application", + "radix-api", + "--context", + "playground", + "-e", + "prod", + "--component", + "server" + ] + }, + { + "name": "Lauch radix-cli follow job", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/main.go", + "env": { + "APP_SERVICE_ACCOUNT_TOKEN": "token" + }, + "args": [ + "--token-environment", + "--context", + "playground", + "follow", + "job", + "--application", + "echo", + "-j", + "radix-pipeline-20191227190153-aysdr" ] } ] diff --git a/cmd/buildDeployApplication.go b/cmd/buildDeployApplication.go index b5a5a79..38bd672 100644 --- a/cmd/buildDeployApplication.go +++ b/cmd/buildDeployApplication.go @@ -16,29 +16,13 @@ package cmd import ( "errors" - "fmt" - "strings" - "time" "github.com/equinor/radix-cli/generated-client/client/application" - "github.com/equinor/radix-cli/generated-client/client/job" "github.com/equinor/radix-cli/generated-client/models" "github.com/equinor/radix-cli/pkg/client" - "github.com/fatih/color" "github.com/spf13/cobra" ) -const deltaRefreshApplication = 3 * time.Second -const deltaRefreshOutput = 50 * time.Millisecond - -var ( - yellow = color.New(color.FgHiYellow, color.BgBlack, color.Bold).SprintFunc() - green = color.New(color.FgHiGreen, color.BgBlack, color.Bold).SprintFunc() - blue = color.New(color.FgHiBlue, color.BgBlack, color.Underline).SprintFunc() - cyan = color.New(color.FgCyan, color.BgBlack).SprintFunc() - red = color.New(color.FgHiRed, color.BgBlack).Add(color.Italic).SprintFunc() -) - // buildDeployApplicationCmd represents the buildApplication command var buildDeployApplicationCmd = &cobra.Command{ Use: "build-deploy", @@ -75,64 +59,9 @@ var buildDeployApplicationCmd = &cobra.Command{ return err } + jobName := newJob.GetPayload().Name if follow { - jobName := newJob.GetPayload().Name - - jobParameters := job.NewGetApplicationJobParams() - jobParameters.SetAppName(*appName) - jobParameters.SetJobName(jobName) - - fmt.Fprintf(cmd.OutOrStdout(), "\r%s", fmt.Sprintf("Building %s on branch %s with name %s", cyan(appName), yellow(branch), yellow(jobName))) - - buildComplete := false - - numLogLinesOutput := 0 - refreshApplication := time.Tick(deltaRefreshApplication) - - for { - select { - case <-refreshApplication: - jobLogParameters := job.NewGetApplicationJobLogsParams() - jobLogParameters.SetAppName(*appName) - jobLogParameters.SetJobName(jobName) - - respJobLog, _ := apiClient.Job.GetApplicationJobLogs(jobLogParameters, nil) - if respJobLog != nil { - numLogLines := 0 - - stepsLog := respJobLog.Payload - for _, stepLog := range stepsLog { - stepLogLines := strings.Split(strings.Replace(stepLog.Log, "\r\n", "\n", -1), "\n") - - for _, stepLogLine := range stepLogLines { - if numLogLinesOutput <= numLogLines { - fmt.Fprintf(cmd.OutOrStdout(), "\r\n%s", stepLogLine) - numLogLinesOutput++ - } - - numLogLines++ - } - } - } - - respJob, _ := apiClient.Job.GetApplicationJob(jobParameters, nil) - if respJob != nil { - jobSummary := respJob.Payload - if jobSummary.Status == "Succeeded" { - fmt.Fprintf(cmd.OutOrStdout(), fmt.Sprintf("%s", green("\nBuild complete\n"))) - buildComplete = true - } else if jobSummary.Status == "Failed" { - fmt.Fprintf(cmd.OutOrStdout(), fmt.Sprintf("%s", red("\nBuild failed\n"))) - buildComplete = true - } - - if buildComplete { - return nil - } - } - } - - } + followJob(cmd, apiClient, *appName, jobName) } return nil diff --git a/cmd/follow.go b/cmd/follow.go new file mode 100644 index 0000000..913117c --- /dev/null +++ b/cmd/follow.go @@ -0,0 +1,44 @@ +// Copyright © 2019 NAME HERE +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "errors" + "time" + + "github.com/spf13/cobra" +) + +const ( + deltaRefreshApplication = 3 * time.Second + deltaTimeout = 30 * time.Second + deltaRefreshOutput = 50 * time.Millisecond +) + +// followCmd represents the list command +var followCmd = &cobra.Command{ + Use: "follow", + Short: "Follow Radix resources", + Long: `A longer description .`, + RunE: func(cmd *cobra.Command, args []string) error { + return errors.New("Please specify the resource you want to follow") + }, +} + +func init() { + rootCmd.AddCommand(followCmd) + followCmd.AddCommand(followEnvironmentComponentCmd) + followCmd.AddCommand(followJobCmd) +} diff --git a/cmd/followEnvironmentComponent.go b/cmd/followEnvironmentComponent.go new file mode 100644 index 0000000..65ba8d0 --- /dev/null +++ b/cmd/followEnvironmentComponent.go @@ -0,0 +1,137 @@ +// Copyright © 2019 NAME HERE +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "errors" + "strings" + "time" + + apiclient "github.com/equinor/radix-cli/generated-client/client" + "github.com/equinor/radix-cli/generated-client/client/component" + "github.com/equinor/radix-cli/generated-client/client/environment" + "github.com/equinor/radix-cli/pkg/client" + "github.com/equinor/radix-cli/pkg/utils/log" + "github.com/spf13/cobra" +) + +// followEnvironmentComponentCmd represents the followEnvironmentComponentCmd command +var followEnvironmentComponentCmd = &cobra.Command{ + Use: "component", + Short: "Will follow a component in an environment", + Long: `Will follow a component in an environment`, + RunE: func(cmd *cobra.Command, args []string) error { + appName, err := getAppNameFromConfigOrFromParameter(cmd, "application") + if err != nil { + return err + } + + if appName == nil || *appName == "" { + return errors.New("Application name is required") + } + + environmentName, _ := cmd.Flags().GetString("environment") + componentName, _ := cmd.Flags().GetString("component") + + if environmentName == "" || componentName == "" { + return errors.New("Both `environment` and `component` are required") + } + + apiClient, err := client.GetForCommand(cmd) + if err != nil { + return err + } + + deploymentName, replicas, err := getReplicasForComponent(apiClient, *appName, environmentName, componentName) + if err != nil { + return err + } + + refreshLog := time.Tick(deltaRefreshApplication) + loggedForReplica := make(map[string]int) + + for { + select { + case <-refreshLog: + + for i, replica := range replicas { + logParameters := component.NewLogParams() + logParameters.WithAppName(*appName) + logParameters.WithDeploymentName(*deploymentName) + logParameters.WithComponentName(componentName) + logParameters.WithPodName(replica) + + logData, err := apiClient.Component.Log(logParameters, nil) + if err != nil { + // Replicas may have died + deploymentName, replicas, err = getReplicasForComponent(apiClient, *appName, environmentName, componentName) + if err != nil { + return err + } + + } else { + totalLinesLogged := 0 + + if _, contained := loggedForReplica[replica]; contained { + totalLinesLogged = loggedForReplica[replica] + } + + logLines := strings.Split(strings.Replace(logData.Payload, "\r\n", "\n", -1), "\n") + logged := log.From(cmd, replica, totalLinesLogged, logLines, log.GetColor(i)) + + totalLinesLogged += logged + loggedForReplica[replica] = totalLinesLogged + } + } + } + + } + }, +} + +func getReplicasForComponent(apiClient *apiclient.Radixapi, appName, environmentName, componentName string) (*string, []string, error) { + // Get active deployment + environmentParams := environment.NewGetEnvironmentParams() + environmentParams.SetAppName(appName) + environmentParams.SetEnvName(environmentName) + environmentDetails, err := apiClient.Environment.GetEnvironment(environmentParams, nil) + + if err != nil { + return nil, nil, err + } + + var deploymentName string + if environmentDetails == nil || environmentDetails.Payload.ActiveDeployment == nil { + return nil, nil, errors.New("Active deployment was not found in environment") + } + + var replicas []string + deploymentName = environmentDetails.Payload.ActiveDeployment.Name + for _, component := range environmentDetails.Payload.ActiveDeployment.Components { + if component.Name != nil && + *component.Name == componentName { + replicas = component.Replicas + break + } + } + + return &deploymentName, replicas, nil +} + +func init() { + followEnvironmentComponentCmd.Flags().StringP("application", "a", "", "Name of the application owning the component") + followEnvironmentComponentCmd.Flags().StringP("environment", "e", "", "Environment the component runs in") + followEnvironmentComponentCmd.Flags().String("component", "", "The component to follow") +} diff --git a/cmd/followJob.go b/cmd/followJob.go new file mode 100644 index 0000000..368117c --- /dev/null +++ b/cmd/followJob.go @@ -0,0 +1,134 @@ +// Copyright © 2019 NAME HERE +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "errors" + "fmt" + "strings" + "time" + + apiclient "github.com/equinor/radix-cli/generated-client/client" + "github.com/equinor/radix-cli/generated-client/client/job" + "github.com/equinor/radix-cli/generated-client/models" + "github.com/equinor/radix-cli/pkg/client" + "github.com/equinor/radix-cli/pkg/utils/log" + "github.com/spf13/cobra" +) + +// followJobCmd represents the followJobCmd command +var followJobCmd = &cobra.Command{ + Use: "job", + Short: "Will follow a job", + Long: `Will follow a job`, + RunE: func(cmd *cobra.Command, args []string) error { + appName, err := getAppNameFromConfigOrFromParameter(cmd, "application") + if err != nil { + return err + } + + if appName == nil || *appName == "" { + return errors.New("Application name is required") + } + + jobName, _ := cmd.Flags().GetString("job") + + if jobName == "" { + return errors.New("`job` is required") + } + + apiClient, err := client.GetForCommand(cmd) + if err != nil { + return err + } + + followJob(cmd, apiClient, *appName, jobName) + return nil + }, +} + +func followJob(cmd *cobra.Command, apiClient *apiclient.Radixapi, appName, jobName string) { + timeout := time.NewTimer(deltaTimeout) + refreshLog := time.Tick(deltaRefreshApplication) + loggedForStep := make(map[string]int) + + for { + select { + case <-refreshLog: + + loggedForJob := false + steps := getSteps(apiClient, appName, jobName) + + for i, step := range steps { + totalLinesLogged := 0 + + if _, contained := loggedForStep[*step.Name]; contained { + totalLinesLogged = loggedForStep[*step.Name] + } + + logLines := strings.Split(strings.Replace(step.Log, "\r\n", "\n", -1), "\n") + logged := log.From(cmd, *step.Name, totalLinesLogged, logLines, log.GetColor(i)) + + totalLinesLogged += logged + loggedForStep[*step.Name] = totalLinesLogged + + if logged > 0 { + loggedForJob = true + } + } + + if loggedForJob { + // Reset timeout + timeout = time.NewTimer(deltaTimeout) + } + case <-timeout.C: + jobParameters := job.NewGetApplicationJobParams() + jobParameters.SetAppName(appName) + jobParameters.SetJobName(jobName) + + respJob, _ := apiClient.Job.GetApplicationJob(jobParameters, nil) + if respJob != nil { + jobSummary := respJob.Payload + if jobSummary.Status == "Succeeded" { + log.Print(cmd, "radix-cli", "Build complete", log.Green) + } else if jobSummary.Status == "Failed" { + log.Print(cmd, "radix-cli", "Build failed", log.Red) + } else { + log.Print(cmd, "radix-cli", fmt.Sprintf("Nothing logged the last %s. Timeout", deltaTimeout), log.GetColor(0)) + } + } + + return + } + } +} + +func getSteps(apiClient *apiclient.Radixapi, appName, jobName string) []*models.StepLog { + jobLogParameters := job.NewGetApplicationJobLogsParams() + jobLogParameters.SetAppName(appName) + jobLogParameters.SetJobName(jobName) + + respJobLog, err := apiClient.Job.GetApplicationJobLogs(jobLogParameters, nil) + if err == nil { + return respJobLog.Payload + } + + return nil +} + +func init() { + followJobCmd.Flags().StringP("application", "a", "", "Name of the application owning the component") + followJobCmd.Flags().StringP("job", "j", "", "The job to follow") +} diff --git a/go.mod b/go.mod index 1f9afbb..babbc53 100644 --- a/go.mod +++ b/go.mod @@ -13,12 +13,15 @@ require ( github.com/go-openapi/validate v0.19.5 github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.11 // indirect + github.com/prometheus/common v0.7.0 github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.3 go.opencensus.io v0.22.2 // indirect golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect + google.golang.org/appengine v1.6.5 + k8s.io/api v0.0.0-20191016225839-816a9b7df678 k8s.io/apimachinery v0.0.0-20191020214737-6c8691705fc5 k8s.io/client-go v12.0.0+incompatible ) diff --git a/go.sum b/go.sum index 4e8886e..9bc4fcc 100644 --- a/go.sum +++ b/go.sum @@ -65,9 +65,11 @@ github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmx github.com/a8m/mark v0.1.1-0.20170507133748-44f2db618845/go.mod h1:c8Mh99Cw82nrsAnPgxQSZHkswVOJF7/MqZb1ZdvriLM= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/ant31/crd-validation v0.0.0-20180702145049-30f8a35d0ac2/go.mod h1:X0noFIik9YqfhGYBLEHg8LJKEwy7QIitLQuFMpKLcPk= @@ -730,6 +732,7 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170731182057-09f6ed296fc6/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -754,6 +757,7 @@ google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index 5763f33..5b283ad 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,14 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth/azure" ) +func init() { + // If you get GOAWAY calling API with token using: + // az account get-access-token + // ...enable this line + // os.Setenv("GODEBUG", "http2server=0,http2client=0") + +} + func main() { cmd.Execute() } diff --git a/pkg/utils/log/log_util.go b/pkg/utils/log/log_util.go new file mode 100644 index 0000000..3b87a8c --- /dev/null +++ b/pkg/utils/log/log_util.go @@ -0,0 +1,47 @@ +package log + +import ( + "fmt" + "strings" + + "github.com/fatih/color" + "github.com/spf13/cobra" +) + +var ( + Yellow = color.New(color.FgHiYellow, color.BgBlack, color.Bold).SprintFunc() + Green = color.New(color.FgHiGreen, color.BgBlack, color.Bold).SprintFunc() + Blue = color.New(color.FgHiBlue, color.BgBlack, color.Underline).SprintFunc() + Cyan = color.New(color.FgCyan, color.BgBlack).SprintFunc() + Red = color.New(color.FgHiRed, color.BgBlack).Add(color.Italic).SprintFunc() + Magenta = color.New(color.FgHiMagenta, color.BgBlack).Add(color.Italic).SprintFunc() + + Colors = []func(a ...interface{}) string{Yellow, Green, Blue, Cyan, Red, Magenta} +) + +// GetColor Rotates color +func GetColor(num int) func(a ...interface{}) string { + return Colors[num%len(Colors)] +} + +// From logs lines exceeding from +func From(cmd *cobra.Command, name string, from int, logLines []string, color func(a ...interface{}) string) int { + logged := 0 + + for num, logLine := range logLines { + if num >= from { + if !strings.EqualFold(strings.TrimSpace(logLine), "") && from > 0 { + Print(cmd, name, logLine, color) + } + + logged++ + } + } + + return logged +} + +// Print Output string to standard output +func Print(cmd *cobra.Command, name, logLine string, color func(a ...interface{}) string) { + fmt.Fprintf(cmd.OutOrStdout(), "\r\n[%s] : %s", color(name), color(logLine)) +}