Skip to content

Commit

Permalink
feat: Added Smart Mode that Automatically detects changed Environment… (
Browse files Browse the repository at this point in the history
#62)

…s and Applications

---------

Co-authored-by: Fritz Durchardt <fritz.duchardt.ext@gec.io>
  • Loading branch information
fritzduchardt and Fritz Durchardt authored Sep 7, 2023
1 parent 06e5e5c commit e404b6b
Show file tree
Hide file tree
Showing 15 changed files with 1,208 additions and 10 deletions.
34 changes: 34 additions & 0 deletions SMARTMODE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Smart Mode

Smart Mode looks at the changes you made and figures out which environments and applications need re-rendering.

It is activated by default.

## How it works

### No changes relative to rendering

If you make changes that have no impact on the rendered output of your workloads, e.g. modify `Dockerfile`, myks
will exit immediately, before any syncing or rendering happens.

### Re-rendering of an application

An application of a specific environment get re-rendered when:
- The `app-data.ytt.yaml` of that application has changed, e.g. `envs/env-1/_apps/app-1/app-data.ytt.yaml`
- The prototype application has changed, e.g. `prototypes/app-1/helm/app-1.yaml`, in which case all environments that use is re-render that application.

### Re-rendering of an environment

All applications of an environment get re-rendered when:
- The `env-data.ytt.yaml` of that environment has changed.
- The `env-data.ytt.yaml` of a parent environment of that environment has changed.
- **Edge case:** If you have made changes to an application in env-1, but at the same time have modified the `env-data.ytt.yaml` of env-2, smart-mode will re-render all applications of env-1 AND env-2, even though this is not strictly required for env-1.

### Complete rendering

A complete rendering of all environments and all applications is required when:
- A file in the common lib has changed, e.g. `/lib/common.lib.star`
- A file in the global ytt folder has changed, e.g. `/envs/_env/ytt/annotate_crds.yaml`
- The base `env-data.ytt.yaml` has changed: `/envs/env-data.ytt.yaml`


2 changes: 1 addition & 1 deletion Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ tasks:

go:lint:sec:
cmds:
- gosec ./...
- gosec -exclude=G304 ./... # G304: Potential file inclusion via variable is a false positive

go:lint:
desc: Lint the code
Expand Down
28 changes: 25 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/mykso/myks/internal/myks"
)

var (
Expand All @@ -24,8 +26,18 @@ var (
var rootCmd = &cobra.Command{
Use: "myks",
Short: "Myks helps to manage configuration for kubernetes clusters",
Long: "Myks TBD",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
Long: `Myks fetches K8s workloads from a variety of sources, e.g. Helm charts or Git Repositories. It renders their respective yaml files to the file system in a structure of environments and their applications.
It supports prototype applications that can be shared between environments and inheritance of configuration from parent environments to their "children".
Myks supports two positional arguments:
- A comma-separated list of environments to render. If you provide "ALL", all environments will be rendered.
- A comma-separated list of applications to render. If you provide "ALL", all applications will be rendered.
If you do not provide any positional arguments, myks will run in "Smart Mode". In Smart Mode, myks will only render environments and applications that have changed since the last run.
`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
// Check positional arguments:
// 1. Comma-separated list of environment search paths or ALL to search everywhere (default: ALL)
// 2. Comma-separated list of application names or none to process all applications (default: none)
Expand All @@ -44,7 +56,17 @@ var rootCmd = &cobra.Command{

switch len(onlyArgs) {
case 0:
// No positional arguments
// smart mode requires instantiation of globe object to get the list of environments
// the globe object will not be used later in the process. It is only used to get the list of all environments and their apps.
globeAllEnvsAndApps := myks.New(".")
targetEnvironments, targetApplications, err = globeAllEnvsAndApps.InitSmartMode()
if err != nil {
log.Warn().Err(err).Msg("Unable to run Smart Mode. Rendering everything.")
}
if targetEnvironments == nil && targetApplications == nil {
log.Warn().Msg("Smart Mode did not find any changes. Existing.")
os.Exit(0)
}
case 1:
if onlyArgs[0] != "ALL" {
targetEnvironments = strings.Split(onlyArgs[0], ",")
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.9.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
Expand Down Expand Up @@ -329,6 +331,8 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
1 change: 0 additions & 1 deletion internal/myks/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ func NewEnvironment(g *Globe, dir string) *Environment {
// Read an environment id from an environment data file.
// The environment data file must exist and contain an .environment.id field.
if err := env.setId(); err != nil {
log.Warn().Err(err).Str("dir", dir).Msg("Unable to set environment id")
return nil
}

Expand Down
112 changes: 112 additions & 0 deletions internal/myks/git.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package myks

import (
"fmt"
"os"
"strings"

"github.com/rs/zerolog/log"
)

type ChangedFile struct {
path string
status string
}

// return all files paths that were changed since the revision
func getChangedFiles(revision string) ([]ChangedFile, error) {
log := func(name string, args []string) {
log.Debug().Msg(msgRunCmd("get diff for smart-mode", name, args))
}
_, err := runCmd("git", nil, []string{"add", ".", "--intent-to-add"}, log)
if err != nil {
return nil, err
}
result, err := runCmd("git", nil, []string{"diff", "--ignore-blank-lines", "--name-status", revision}, log)
if err != nil {
return nil, err
}
if result.Stdout == "" {
return nil, nil
}
return convertToChangedFiles(result.Stdout), err
}

func convertToChangedFiles(changes string) []ChangedFile {
var cfs []ChangedFile
for _, str := range strings.Split(changes, "\n") {
if str != "" {
parts := strings.Split(str, "\t")
cf := ChangedFile{path: parts[1], status: parts[0]}
cfs = append(cfs, cf)
}
}
return cfs
}

func extractChangedFilePaths(cfs []ChangedFile) []string {
var paths []string
for _, cf := range cfs {
paths = append(paths, cf.path)
}
return paths
}

func extractChangedFilePathsWithStatus(cfs []ChangedFile, status string) []string {
filter := func(cf ChangedFile) bool {
if status == "" || cf.status == status {
return true
}
return false
}
return extractChangedFilePaths(extract(cfs, filter))
}

func extractChangedFilePathsWithoutStatus(cfs []ChangedFile, status string) []string {
filter := func(cf ChangedFile) bool {
if status == "" || cf.status != status {
return true
}
return false
}
return extractChangedFilePaths(extract(cfs, filter))
}

// get head revision of main branch
func getMainBranchHeadRevision(mainBranch string) (string, error) {
log := func(name string, args []string) {
log.Debug().Msg(msgRunCmd("get main branch head revision for smart-mode", name, args))
}
_, err := runCmd("git", nil, []string{"fetch", "origin", mainBranch}, log)
if err != nil {
return "", err
}
cmdResult, err := runCmd("git", nil, []string{"merge-base", "origin/" + mainBranch, "HEAD"}, log)
if err != nil {
return "", err
}
// git adds new line to output which messes up the result
headRevision := strings.TrimRight(cmdResult.Stdout, "\n")
return headRevision, nil
}

// get head revision
func getCurrentBranchHeadRevision() (string, error) {
log := func(name string, args []string) {
log.Debug().Msg(msgRunCmd("get current head revision for smart-mode", name, args))
}
cmdResult, err := runCmd("git", nil, []string{"rev-parse", "HEAD"}, log)
if err != nil {
return "", fmt.Errorf("failed to get current branch head revision: %v", err)
}
return strings.TrimRight(cmdResult.Stdout, "\n"), nil
}

func getDiffRevision(mainBranch string) (string, error) {
if os.Getenv("CI") != "" {
log.Debug().Msg("Pipeline mode: comparing with HEAD revision on main")
return getMainBranchHeadRevision(mainBranch)
}
log.Debug().Msg("Local mode: comparing with HEAD revision on current branch")
return getCurrentBranchHeadRevision()
}
Loading

0 comments on commit e404b6b

Please sign in to comment.