diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 22a6931..dd30eab 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -24,7 +24,5 @@ jobs: build-args: ${{ matrix.args.build-args }} images: |- ghcr.io/${{ github.repository }}${{ matrix.args.image-suffix }} - # yamllint disable rule:line-length dockle-accept-key: APPLICATION,libcrypto3,libssl3 - # yamllint enable rule:line-length token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/golang.yml b/.github/workflows/golang.yml index 58aa598..2f195c7 100644 --- a/.github/workflows/golang.yml +++ b/.github/workflows/golang.yml @@ -35,7 +35,7 @@ jobs: test-timeout: 10m0s with: build-tags: ${{ matrix.args.build-tags }} - code-coverage-expected: 25.5 + code-coverage-expected: 24.4 code-coverage-timeout: ${{ env.test-timeout }} golang-unit-tests-exclusions: |- \(deprecated\|filesystem\|mocks\|swagger\|cmd\/mcvs-.*\|mcvs-\(chart-designer\|reporter\)\)$\? diff --git a/.github/workflows/mcvs-pr-validation.yml b/.github/workflows/mcvs-pr-validation.yml new file mode 100644 index 0000000..a1b5625 --- /dev/null +++ b/.github/workflows/mcvs-pr-validation.yml @@ -0,0 +1,19 @@ +--- +name: MCVS-PR-validation-action +"on": + pull_request: + types: + - edited + - opened + - reopened + - synchronize + workflow_call: +permissions: + contents: read + pull-requests: read +jobs: + MCVS-PR-validation-action: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4.2.2 + - uses: schubergphilis/mcvs-pr-validation-action@v0.2.0 diff --git a/.gitignore b/.gitignore index 1892898..dd66b8b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ +*.snap +.task cmd/dip/dip.sha512.txt coverage.txt dip -*.snap -.task +profile.cov +unique_profile.cov diff --git a/cmd/dip/main.go b/cmd/dip/main.go index a8972a4..d169702 100644 --- a/cmd/dip/main.go +++ b/cmd/dip/main.go @@ -17,26 +17,21 @@ import ( "github.com/spf13/viper" ) -func main() { - Execute() -} - const ext = "yml" var ( - cfgCredHome, Version string - debug bool + cfgCredHome, Version string + debug bool + dockerfile, k8sfile bool + kubernetes, quayIo bool + sendSlackMsg, updateDockerfile bool + name, regex string ) var rootCmd = &cobra.Command{ - Use: "dip", - Short: "A brief description of your application", - Long: `A longer description that spans multiple lines and likely contains -examples and usage of using your application. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, + Use: "dip", + Short: "A Docker/Kubernetes image policy enforcement CLI", + Long: "dip helps enforce up-to-date image tags in Dockerfiles and Kubernetes manifests. It also supports Slack notifications and regex-based tag matching from DockerHub and Quay.io.", Version: Version, Run: func(cmd *cobra.Command, args []string) { if debug { @@ -46,12 +41,142 @@ to quickly create a Cobra application.`, }, } -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. +var imageCmd = &cobra.Command{ + Use: "image", + Short: "Verify and update container image tags", + Long: "The image subcommand validates Docker and Kubernetes image tags against latest tags from DockerHub or Quay.io. It can also send Slack notifications if tags are outdated.", + Run: func(cmd *cobra.Command, args []string) { + latestTag, err := fetchLatestTag() + if err != nil { + log.Fatal(err) + } + fmt.Println(latestTag) + + if dockerfile { + err = docker.FileLatest(name, latestTag) + if err != nil { + sendSlackIfOutdated(latestTag) + log.Fatal(err) + } + } + + if updateDockerfile { + err = docker.UpdateFROMStatementDockerfile(name, latestTag) + if err != nil { + log.Fatal(err) + } + } + + if k8sfile { + tag, err := k8s.FileTag(name) + if err != nil { + log.Fatal(err) + } + if latestTag != tag { + log.Fatal(fmt.Errorf("k8sfile tag: '%s' seems to be outdated, as: '%s' exists. Please update the tag in the k8sfile", tag, latestTag)) + } + log.Infof("k8sfile tag: '%s' is up to date. Latest: '%v'", tag, latestTag) + } + + if kubernetes { + images, err := imagesToBeValidated() + if err != nil { + log.Fatal(err) + } + token, err := slackToken() + if err != nil { + log.Fatal(err) + } + channelID, err := slackChannelID() + if err != nil { + log.Fatal(err) + } + k := k8s.Images{ToBeValidated: images, SlackToken: token, SlackChannelID: channelID} + err = k.UpToDate() + if err != nil { + log.Fatal(err) + } + } + }, +} + +func main() { + Execute() +} + func Execute() { cobra.CheckErr(rootCmd.Execute()) } +func init() { + cobra.OnInitialize(initConfig) + + rootCmd.PersistentFlags().StringVar(&cfgCredHome, "configCredHome", "", "Config and credential file home directory (default is $HOME/.dip)") + rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug mode") + + imageCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the Docker image to check") + cobra.CheckErr(imageCmd.MarkFlagRequired("name")) + + imageCmd.Flags().StringVarP(®ex, "regex", "r", "", "Regex to find the latest image tag") + cobra.CheckErr(imageCmd.MarkFlagRequired("regex")) + + imageCmd.Flags().BoolVar(&dockerfile, "dockerfile", false, "Check if Dockerfile image tag is outdated") + imageCmd.Flags().BoolVar(&updateDockerfile, "updateDockerfile", false, "Update Dockerfile FROM statement") + imageCmd.Flags().BoolVar(&k8sfile, "k8sfile", false, "Check if Kubernetes manifest image tags are outdated") + imageCmd.Flags().BoolVar(&kubernetes, "kubernetes", false, "Run full Kubernetes image tag validation") + imageCmd.Flags().BoolVar(&quayIo, "quayIo", false, "Check tags on Quay.io instead of DockerHub") + imageCmd.Flags().BoolVar(&sendSlackMsg, "sendSlackMsg", false, "Send Slack message when outdated image is found") + + rootCmd.AddCommand(imageCmd) +} + +func initConfig() { + log.SetReportCaller(true) + if debug { + log.SetLevel(log.DebugLevel) + jww.SetLogThreshold(jww.LevelTrace) + jww.SetStdoutThreshold(jww.LevelDebug) + } +} + +func fetchLatestTag() (string, error) { + if quayIo { + q := quay.Quay{ + HTTPGetter: quay.HTTPGet{}, + Image: name, + } + return q.LatestTagBasedOnRegex(regex, name) + } + return dockerhub.LatestTagBasedOnRegex(regex, name) +} + +func sendSlackIfOutdated(latestTag string) { + if !sendSlackMsg { + return + } + + channelID, err := slackChannelID() + if err != nil { + log.Fatal(err) + } + + token, err := slackToken() + if err != nil { + log.Fatal(err) + } + + msg := fmt.Sprintf("Image: '%s' in Dockerfile outdated. Latest tag: '%s'", name, latestTag) + if os.Getenv("GITLAB_CI") == "true" { + msg = fmt.Sprintf("%s. CI_PROJECT_PATH: '%s'. BRANCH: '%s'. CI_PROJECT_URL: '%s'", + msg, os.Getenv("CI_PROJECT_PATH"), os.Getenv("CI_COMMIT_BRANCH"), os.Getenv("CI_PROJECT_URL")) + } + + err = slack.SendMessage(channelID, msg, token) + if err != nil { + log.Fatal(err) + } +} + func viperBase(filename string) error { home, err := homedir.Dir() if err != nil { @@ -66,19 +191,21 @@ func viperBase(filename string) error { viper.AddConfigPath(cfgCredHome) } - if err := viper.ReadInConfig(); err != nil { + err = viper.ReadInConfig() + if err != nil { return fmt.Errorf("fatal error config file: %v", err) } return nil } func credsValue(key string) (string, error) { - if err := viperBase("creds"); err != nil { + err := viperBase("creds") + if err != nil { return "", err } value := viper.GetString(key) if value == "" { - return "", fmt.Errorf("no "+key+" found. Check whether the '"+key+"' variable is populated in '%s'", viper.ConfigFileUsed()) + return "", fmt.Errorf("no %s found. Check whether the '%s' variable is populated in '%s'", key, key, viper.ConfigFileUsed()) } return value, nil } @@ -92,7 +219,8 @@ func slackChannelID() (string, error) { } func imagesToBeValidated() (map[string]interface{}, error) { - if err := viperBase("config"); err != nil { + err := viperBase("config") + if err != nil { return nil, err } images := viper.GetStringMap("dip_images") @@ -101,141 +229,3 @@ func imagesToBeValidated() (map[string]interface{}, error) { } return images, nil } - -func init() { - cobra.OnInitialize(initConfig) - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, - // will be global for your application. - - rootCmd.PersistentFlags().StringVar(&cfgCredHome, "configCredHome", "", "config and cred file home directory (default is $HOME/.dip)") - rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "debugging mode") -} - -func initConfig() { - enableDebug() -} - -func enableDebug() { - log.SetReportCaller(true) - if debug { - log.SetLevel(log.DebugLevel) - - // Added to be able to debug viper (used to read the config file) - // Viper is using a different logger - jww.SetLogThreshold(jww.LevelTrace) - jww.SetStdoutThreshold(jww.LevelDebug) - } -} - -var ( - dockerfile, k8sfile, kubernetes, quayIo, sendSlackMsg, updateDockerfile bool - name, regex string - imageCmd = &cobra.Command{ - Use: "image", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - var err error - latestTag := "" - if quayIo { - quay := quay.Quay{ - HTTPGetter: quay.HTTPGet{}, - Image: name, - } - latestTag, err = quay.LatestTagBasedOnRegex(regex, name) - } else { - latestTag, err = dockerhub.LatestTagBasedOnRegex(regex, name) - } - if err != nil { - log.Fatal(err) - } - - // fmt is used to ensure that only the tag is returned - fmt.Println(latestTag) - - channelID := "" - token := "" - if sendSlackMsg { - channelID, err = slackChannelID() - if err != nil { - log.Fatal(err) - } - token, err = slackToken() - if err != nil { - log.Fatal(err) - } - } - - if dockerfile { - if err := docker.FileLatest(name, latestTag); err != nil { - if sendSlackMsg { - msg := fmt.Sprintf("Image: '%s' in Dockerfile outdated. Latest tag: '%s'", name, latestTag) - if os.Getenv("GITLAB_CI") == "true" { - msg = fmt.Sprintf("%s. CI_PROJECT_PATH: '%s'. BRANCH: '%s'. CI_PROJECT_URL: '%s'", msg, os.Getenv("CI_PROJECT_PATH"), os.Getenv("CI_COMMIT_BRANCH"), os.Getenv("CI_PROJECT_URL")) - } - if err := slack.SendMessage(channelID, msg, token); err != nil { - log.Fatal(err) - } - } - log.Fatal(err) - } - } - - if updateDockerfile { - if err := docker.UpdateFROMStatementDockerfile(name, latestTag); err != nil { - log.Fatal(err) - } - } - - if k8sfile { - dft, err := k8s.FileTag(name) - if err != nil { - log.Fatal(err) - } - if latestTag != dft { - log.Fatal(fmt.Errorf("k8sfile tag: '%s' seems to be outdated, as: '%s' exists. Please update the tag in the k8sfile", dft, latestTag)) - } - log.Infof("k8sfile tag: '%s' is up to date. Latest: '%v'", dft, latestTag) - } - - if kubernetes { - images, err := imagesToBeValidated() - if err != nil { - log.Fatal(err) - } - - k := k8s.Images{ToBeValidated: images, SlackToken: token, SlackChannelID: channelID} - if err := k.UpToDate(); err != nil { - log.Fatal(err) - } - } - }, - } -) - -func init() { - rootCmd.AddCommand(imageCmd) - - imageCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the Docker image to be checked whether it is up to date") - if err := imageCmd.MarkFlagRequired("name"); err != nil { - log.Fatal(err) - } - - imageCmd.Flags().StringVarP(®ex, "regex", "r", "", "Regex for finding the latest image tag") - if err := imageCmd.MarkFlagRequired("regex"); err != nil { - log.Fatal(err) - } - - imageCmd.Flags().BoolVar(&dockerfile, "dockerfile", false, "Check whether the image that resides in the Dockerfile is outdated") - imageCmd.Flags().BoolVar(&k8sfile, "k8sfile", false, "Check whether the images that resides in the k8sfiles are outdated") - imageCmd.Flags().BoolVar(&kubernetes, "kubernetes", false, "Check whether the image in a k8s file is outdated") - imageCmd.Flags().BoolVar(&quayIo, "quayIo", false, "Check the latest tag on quay.io") - imageCmd.Flags().BoolVar(&sendSlackMsg, "sendSlackMsg", false, "Send message to Slack") - imageCmd.Flags().BoolVar(&updateDockerfile, "updateDockerfile", false, "Update the FROM image that resides in the Dockerfile") -}