diff --git a/cmd/fetcher.go b/cmd/fetcher.go new file mode 100644 index 0000000..246cbd7 --- /dev/null +++ b/cmd/fetcher.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "net/http" + "time" + + "github.com/spf13/cobra" +) + +var fetchCmd = &cobra.Command{ + Use: "fetch", + Short: "Trigger a fetch of all Git remotes", + Args: cobra.MinimumNArgs(0), + Run: func(cmd *cobra.Command, args []string) { + url := "http://localhost:4242/api/fetcher/fetch" + client := http.Client{ + Timeout: time.Second * 2, + } + req, err := http.NewRequest(http.MethodPost, url, nil) + if err != nil { + return + } + client.Do(req) + }, +} + +func init() { + rootCmd.AddCommand(fetchCmd) +} diff --git a/cmd/run.go b/cmd/run.go index bc198d1..7cfa949 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -3,13 +3,18 @@ package cmd import ( "os" "path" + "time" + "github.com/nlewo/comin/internal/builder" "github.com/nlewo/comin/internal/config" + "github.com/nlewo/comin/internal/deployer" + "github.com/nlewo/comin/internal/fetcher" "github.com/nlewo/comin/internal/http" "github.com/nlewo/comin/internal/manager" - "github.com/nlewo/comin/internal/poller" + "github.com/nlewo/comin/internal/nix" "github.com/nlewo/comin/internal/prometheus" "github.com/nlewo/comin/internal/repository" + "github.com/nlewo/comin/internal/scheduler" store "github.com/nlewo/comin/internal/store" "github.com/nlewo/comin/internal/utils" "github.com/sirupsen/logrus" @@ -46,17 +51,27 @@ var runCmd = &cobra.Command{ // We get the last mainCommitId to avoid useless // redeployment as well as non fast forward checkouts var mainCommitId string - if ok, lastDeployment := store.LastDeployment(); ok { - mainCommitId = lastDeployment.Generation.MainCommitId + var lastDeployment *deployer.Deployment + if ok, ld := store.LastDeployment(); ok { + mainCommitId = ld.Generation.MainCommitId + lastDeployment = &ld } - repository, err := repository.New(gitConfig, mainCommitId) + repository, err := repository.New(gitConfig, mainCommitId, metrics) if err != nil { logrus.Errorf("Failed to initialize the repository: %s", err) os.Exit(1) } - manager := manager.New(repository, store, metrics, gitConfig.Path, gitConfig.Dir, cfg.Hostname, machineId) - go poller.Poller(manager, cfg.Remotes) + fetcher := fetcher.NewFetcher(repository) + fetcher.Start() + sched := scheduler.New() + sched.FetchRemotes(fetcher, cfg.Remotes) + + builder := builder.New(gitConfig.Path, gitConfig.Dir, cfg.Hostname, 5*time.Minute, nix.Eval, 30*time.Minute, nix.Build) + deployer := deployer.New(nix.Deploy, lastDeployment) + + manager := manager.New(store, metrics, sched, fetcher, builder, deployer, machineId) + http.Serve(manager, metrics, cfg.ApiServer.ListenAddress, cfg.ApiServer.Port, diff --git a/cmd/status.go b/cmd/status.go index 6932a7c..908853c 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -9,64 +9,14 @@ import ( "github.com/dustin/go-humanize" - "github.com/nlewo/comin/internal/deployment" - "github.com/nlewo/comin/internal/generation" + "github.com/nlewo/comin/internal/builder" "github.com/nlewo/comin/internal/manager" - "github.com/nlewo/comin/internal/utils" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -func generationStatus(g generation.Generation) { - fmt.Printf(" Current Generation\n") - switch g.Status { - case generation.Init: - fmt.Printf(" Status: initializated\n") - case generation.Evaluating: - fmt.Printf(" Status: evaluating (since %s)\n", humanize.Time(g.EvalStartedAt)) - case generation.EvaluationSucceeded: - fmt.Printf(" Status: evaluated (%s)\n", humanize.Time(g.EvalEndedAt)) - case generation.Building: - fmt.Printf(" Status: building (since %s)\n", humanize.Time(g.BuildStartedAt)) - case generation.BuildSucceeded: - fmt.Printf(" Status: built (%s)\n", humanize.Time(g.BuildEndedAt)) - case generation.EvaluationFailed: - fmt.Printf(" Status: evaluation failed (%s)\n", humanize.Time(g.EvalEndedAt)) - case generation.BuildFailed: - fmt.Printf(" Status: build failed (%s)\n", humanize.Time(g.BuildEndedAt)) - } - printCommit(g.SelectedRemoteName, g.SelectedBranchName, g.SelectedCommitId, g.SelectedCommitMsg) -} - -func deploymentStatus(d deployment.Deployment) { - fmt.Printf(" Current Deployment\n") - fmt.Printf(" Operation: %s\n", d.Operation) - switch d.Status { - case deployment.Init: - fmt.Printf(" Status: initializated\n") - case deployment.Running: - fmt.Printf(" Status: running (since %s)\n", humanize.Time(d.StartAt)) - case deployment.Done: - fmt.Printf(" Status: succeeded (%s)\n", humanize.Time(d.EndAt)) - case deployment.Failed: - fmt.Printf(" Status: failed (%s)\n", humanize.Time(d.EndAt)) - } - printCommit(d.Generation.SelectedRemoteName, d.Generation.SelectedBranchName, d.Generation.SelectedCommitId, d.Generation.SelectedCommitMsg) -} - -func printCommit(selectedRemoteName, selectedBranchName, selectedCommitId, selectedCommitMsg string) { - fmt.Printf(" Commit %s from '%s/%s'\n", - selectedCommitId, - selectedRemoteName, - selectedBranchName, - ) - fmt.Printf(" %s\n", - utils.FormatCommitMsg(selectedCommitMsg), - ) -} - func getStatus() (status manager.State, err error) { - url := "http://localhost:4242/status" + url := "http://localhost:4242/api/status" client := http.Client{ Timeout: time.Second * 2, } @@ -101,19 +51,21 @@ var statusCmd = &cobra.Command{ if err != nil { logrus.Fatal(err) } - fmt.Printf("Status of the machine %s\n", status.Hostname) + fmt.Printf("Status of the machine %s\n", status.Builder.Hostname) needToReboot := "no" if status.NeedToReboot { needToReboot = "yes" } fmt.Printf(" Need to reboot: %s\n", needToReboot) - for _, r := range status.RepositoryStatus.Remotes { - fmt.Printf(" Remote %s fetched %s\n", - r.Url, humanize.Time(r.FetchedAt), + fmt.Printf(" Fetcher\n") + for _, r := range status.Fetcher.RepositoryStatus.Remotes { + fmt.Printf(" Remote %s %s fetched %s\n", + r.Name, r.Url, humanize.Time(r.FetchedAt), ) } - deploymentStatus(status.Deployment) - generationStatus(status.Generation) + fmt.Printf(" Builder\n") + builder.GenerationShow(*status.Builder.Generation) + status.Deployer.Show(" ") }, } diff --git a/flake.nix b/flake.nix index 64d6fdc..9475f96 100644 --- a/flake.nix +++ b/flake.nix @@ -30,7 +30,7 @@ in { comin = final.buildGoModule rec { pname = "comin"; - version = "0.2.0"; + version = "0.6.0"; nativeCheckInputs = [ final.git ]; src = final.lib.fileset.toSource { root = ./.; @@ -42,7 +42,7 @@ ./main.go ]; }; - vendorHash = "sha256-9qObgfXvMkwE+1BVZNQXVhKhL6LqMqyIUhGnXf8q9SI="; + vendorHash = "sha256-VP8y/iSBIXZFfSmhHsXkp6RxP+2DovX3PbEDtMUMyYE="; ldflags = [ "-X github.com/nlewo/comin/cmd.version=${version}" ]; diff --git a/go.mod b/go.mod index bd40102..777e632 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,13 @@ go 1.22 require ( github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df github.com/dustin/go-humanize v1.0.1 + github.com/go-co-op/gocron/v2 v2.11.0 github.com/go-git/go-git/v5 v5.11.0 - github.com/mattn/go-sqlite3 v1.14.19 + github.com/google/uuid v1.6.0 + github.com/prometheus/client_golang v1.19.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -26,25 +28,27 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jonboulle/clockwork v0.4.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.19.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/skeema/knownhosts v1.2.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.18.0 // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/tools v0.16.1 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/tools v0.22.0 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 5f95a38..0950a46 100644 --- a/go.sum +++ b/go.sum @@ -3,7 +3,6 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -17,14 +16,10 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -34,109 +29,45 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/go-co-op/gocron/v2 v2.11.0 h1:IOowNA6SzwdRFnD4/Ol3Kj6G2xKfsoiiGq2Jhhm9bvE= +github.com/go-co-op/gocron/v2 v2.11.0/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= -github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= -github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= -github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= -github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= -github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= -github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= -github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= -github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= -github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= -github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= -github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= -github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= -github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= -github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= -github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= -github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= -github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= -github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= -github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= -github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= -github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= -github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -149,16 +80,14 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= @@ -168,197 +97,92 @@ github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyh github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= -golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/builder/builder.go b/internal/builder/builder.go new file mode 100644 index 0000000..dcbaf67 --- /dev/null +++ b/internal/builder/builder.go @@ -0,0 +1,237 @@ +package builder + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/google/uuid" + "github.com/nlewo/comin/internal/repository" + "github.com/sirupsen/logrus" +) + +type EvalFunc func(ctx context.Context, flakeUrl string, hostname string) (drvPath string, outPath string, machineId string, err error) +type BuildFunc func(ctx context.Context, drvPath string) error + +type Builder struct { + hostname string + repositoryPath string + repositoryDir string + evalTimeout time.Duration + buildTimeout time.Duration + evalFunc EvalFunc + buildFunc BuildFunc + + mu sync.Mutex + IsEvaluating bool + IsBuilding bool + + generation *Generation + + // EvaluationDone is used to be notified a evaluation is finished. Be careful since only a single goroutine can listen it. + EvaluationDone chan Generation + // BuildDone is used to be notified a build is finished. Be careful since only a single goroutine can listen it. + BuildDone chan Generation + + evaluator Exec + evaluatorWg *sync.WaitGroup + + buildator Exec + buildatorWg *sync.WaitGroup +} + +func New(repositoryPath, repositoryDir, hostname string, evalTimeout time.Duration, evalFunc EvalFunc, buildTimeout time.Duration, buildFunc BuildFunc) *Builder { + logrus.Infof("builder: initialization with repositoryPath=%s, repositoryDir=%s, hostname=%s, evalTimeout=%fs, buildTimeout=%fs, )", + repositoryPath, repositoryDir, hostname, evalTimeout.Seconds(), buildTimeout.Seconds()) + return &Builder{ + repositoryPath: repositoryPath, + repositoryDir: repositoryDir, + hostname: hostname, + evalFunc: evalFunc, + evalTimeout: evalTimeout, + buildFunc: buildFunc, + buildTimeout: buildTimeout, + EvaluationDone: make(chan Generation, 1), + BuildDone: make(chan Generation, 1), + evaluatorWg: &sync.WaitGroup{}, + buildatorWg: &sync.WaitGroup{}, + } +} + +func (b *Builder) GetGeneration() Generation { + b.mu.Lock() + defer b.mu.Unlock() + return *b.generation +} + +type State struct { + Hostname string `json:"is_hostname"` + IsBuilding bool `json:"is_building"` + IsEvaluating bool `json:"is_evaluating"` + Generation *Generation `json:"generation"` +} + +func (b *Builder) State() State { + b.mu.Lock() + defer b.mu.Unlock() + return State{ + Hostname: b.hostname, + IsBuilding: b.IsBuilding, + IsEvaluating: b.IsEvaluating, + Generation: b.generation, + } +} + +// Stop stops the evaluator and the builder is required and wait until +// they have been actually stopped. +func (b *Builder) Stop() { + b.evaluator.Stop() + b.buildator.Stop() + + b.evaluatorWg.Wait() + b.buildatorWg.Wait() + b.mu.Lock() + defer b.mu.Unlock() + b.IsEvaluating = false + b.IsBuilding = false +} + +type Evaluator struct { + flakeUrl string + hostname string + + evalFunc EvalFunc + + drvPath string + outPath string + machineId string +} + +func (r *Evaluator) Run(ctx context.Context) (err error) { + r.drvPath, r.outPath, r.machineId, err = r.evalFunc(ctx, r.flakeUrl, r.hostname) + return err +} + +type Buildator struct { + drvPath string + buildFunc BuildFunc +} + +func (r *Buildator) Run(ctx context.Context) (err error) { + return r.buildFunc(ctx, r.drvPath) +} + +// Eval evaluates a generation. It cancels current any generation +// evaluation or build. +func (b *Builder) Eval(rs repository.RepositoryStatus) { + ctx := context.TODO() + // This is to prempt the builder since we don't need to allow + // several evaluation in parallel + b.Stop() + b.mu.Lock() + defer b.mu.Unlock() + b.IsEvaluating = true + g := Generation{ + UUID: uuid.NewString(), + FlakeUrl: fmt.Sprintf("git+file://%s?dir=%s&rev=%s", b.repositoryPath, b.repositoryDir, rs.SelectedCommitId), + Hostname: b.hostname, + SelectedRemoteName: rs.SelectedRemoteName, + SelectedBranchName: rs.SelectedBranchName, + SelectedCommitId: rs.SelectedCommitId, + SelectedCommitMsg: rs.SelectedCommitMsg, + SelectedBranchIsTesting: rs.SelectedBranchIsTesting, + MainRemoteName: rs.MainBranchName, + MainBranchName: rs.MainBranchName, + MainCommitId: rs.MainCommitId, + EvalStartedAt: time.Now().UTC(), + EvalStatus: Evaluating, + } + b.generation = &g + + evaluator := &Evaluator{ + hostname: g.Hostname, + flakeUrl: g.FlakeUrl, + evalFunc: b.evalFunc, + } + b.evaluator = NewExec(evaluator, b.evalTimeout) + + // This is to wait until the evaluator is stopped + b.evaluatorWg.Add(1) + b.evaluator.Start(ctx) + + go func() { + defer b.evaluatorWg.Done() + b.evaluator.Wait() + b.mu.Lock() + defer b.mu.Unlock() + b.generation.EvalErr = b.evaluator.err + if b.evaluator.err != nil { + b.generation.EvalErrStr = b.evaluator.err.Error() + b.generation.EvalStatus = EvalFailed + } else { + b.generation.EvalStatus = Evaluated + } + b.generation.EvalErr = b.evaluator.err + b.generation.DrvPath = evaluator.drvPath + b.generation.OutPath = evaluator.outPath + b.generation.MachineId = evaluator.machineId + b.generation.EvalEndedAt = time.Now().UTC() + b.IsEvaluating = false + select { + case b.EvaluationDone <- *b.generation: + default: + } + }() +} + +// Build builds a generation which has been previously evaluated. +func (b *Builder) Build() error { + ctx := context.TODO() + b.mu.Lock() + defer b.mu.Unlock() + + if b.generation == nil || b.generation.EvalStatus != Evaluated { + return fmt.Errorf("The generation is not evaluated") + } + if b.IsBuilding { + return fmt.Errorf("The builder is already building") + } + if b.generation.BuildStatus == Built { + return fmt.Errorf("The generation is already built") + } + b.generation.BuildStartedAt = time.Now().UTC() + b.generation.BuildStatus = Building + b.IsBuilding = true + + buildator := &Buildator{ + drvPath: b.generation.DrvPath, + buildFunc: b.buildFunc, + } + b.buildator = NewExec(buildator, b.buildTimeout) + + // This is to wait until the evaluator is stopped + b.buildatorWg.Add(1) + b.buildator.Start(ctx) + + go func() { + defer b.buildatorWg.Done() + b.buildator.Wait() + b.mu.Lock() + defer b.mu.Unlock() + b.generation.BuildEndedAt = time.Now().UTC() + b.generation.BuildErr = b.buildator.err + if b.buildator.err == nil { + b.generation.BuildStatus = Built + } else { + b.generation.BuildStatus = BuildFailed + b.generation.BuildErrStr = b.buildator.err.Error() + } + b.IsBuilding = false + select { + case b.BuildDone <- *b.generation: + default: + } + }() + return nil +} diff --git a/internal/builder/builder_test.go b/internal/builder/builder_test.go new file mode 100644 index 0000000..f55649b --- /dev/null +++ b/internal/builder/builder_test.go @@ -0,0 +1,151 @@ +package builder + +import ( + "context" + "log" + "testing" + "time" + + "github.com/nlewo/comin/internal/repository" + "github.com/stretchr/testify/assert" + + "net/http" + _ "net/http/pprof" +) + +var mkNixEvalMock = func(evalDone chan struct{}) EvalFunc { + return func(ctx context.Context, repositoryPath string, hostname string) (string, string, string, error) { + select { + case <-ctx.Done(): + return "", "", "", ctx.Err() + case <-evalDone: + return "drv-path", "out-path", "", nil + } + } +} + +var mkNixBuildMock = func(buildDone chan struct{}) BuildFunc { + return func(ctx context.Context, drvPath string) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-buildDone: + return nil + } + } +} + +var nixBuildMockNil = func(ctx context.Context, drvPath string) error { return nil } + +func TestBuilderBuild(t *testing.T) { + go func() { + log.Println(http.ListenAndServe("localhost:6060", nil)) + }() + evalDone := make(chan struct{}) + buildDone := make(chan struct{}) + + b := New("", "", "my-machine", 2*time.Second, mkNixEvalMock(evalDone), 2*time.Second, mkNixBuildMock(buildDone)) + + assert.ErrorContains(t, b.Build(), "The generation is not evaluated") + // Run the evaluator + b.Eval(repository.RepositoryStatus{}) + close(evalDone) + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.False(c, b.IsEvaluating) + }, 2*time.Second, 100*time.Millisecond) + + b.Build() + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, b.IsBuilding) + }, 2*time.Second, 100*time.Millisecond) + err := b.Build() + assert.ErrorContains(t, err, "The builder is already building") + + // Stop the evaluator and builder + b.Stop() + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.False(c, b.IsBuilding) + g := b.GetGeneration() + assert.ErrorContains(c, g.BuildErr, "context canceled") + }, 2*time.Second, 100*time.Millisecond) + + // The builder timeouts + err = b.Build() + assert.Nil(t, err) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + g := b.GetGeneration() + assert.ErrorContains(c, g.BuildErr, "context deadline exceeded") + }, 3*time.Second, 100*time.Millisecond) + + // The builder succeeds + err = b.Build() + assert.Nil(t, err) + buildDone <- struct{}{} + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.False(c, b.IsBuilding) + }, 3*time.Second, 100*time.Millisecond) + + // The generation is already built + err = b.Build() + assert.ErrorContains(t, err, "The generation is already built") +} + +func TestEval(t *testing.T) { + evalDone := make(chan struct{}) + b := New("", "", "", 5*time.Second, mkNixEvalMock(evalDone), 5*time.Second, nixBuildMockNil) + b.Eval(repository.RepositoryStatus{}) + assert.True(t, b.IsEvaluating) + + evalDone <- struct{}{} + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.False(c, b.IsEvaluating) + g := b.GetGeneration() + assert.Equal(c, Evaluated, g.EvalStatus) + assert.Equal(c, "drv-path", g.DrvPath) + assert.Equal(c, "out-path", g.OutPath) + }, 2*time.Second, 100*time.Millisecond) +} + +func TestBuilderPreemption(t *testing.T) { + evalDone := make(chan struct{}) + b := New("", "", "", 5*time.Second, mkNixEvalMock(evalDone), 5*time.Second, nixBuildMockNil) + b.Eval(repository.RepositoryStatus{SelectedCommitId: "commit-1"}) + assert.True(t, b.IsEvaluating) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + g := b.GetGeneration() + assert.Equal(c, "commit-1", g.SelectedCommitId) + }, 2*time.Second, 100*time.Millisecond) + + b.Eval(repository.RepositoryStatus{SelectedCommitId: "commit-2"}) + assert.True(t, b.IsEvaluating) + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + g := b.GetGeneration() + assert.Equal(c, "commit-2", g.SelectedCommitId) + }, 2*time.Second, 100*time.Millisecond) +} + +func TestBuilderStop(t *testing.T) { + evalDone := make(chan struct{}) + b := New("", "", "", 5*time.Second, mkNixEvalMock(evalDone), 5*time.Second, nixBuildMockNil) + b.Eval(repository.RepositoryStatus{}) + assert.True(t, b.IsEvaluating) + b.Stop() + assert.EventuallyWithT(t, func(c *assert.CollectT) { + g := b.GetGeneration() + assert.ErrorContains(c, g.EvalErr, "context canceled") + }, 2*time.Second, 100*time.Millisecond) +} + +func TestBuilderTimeout(t *testing.T) { + evalDone := make(chan struct{}) + b := New("", "", "", 1*time.Second, mkNixEvalMock(evalDone), 5*time.Second, nixBuildMockNil) + b.Eval(repository.RepositoryStatus{}) + assert.True(t, b.IsEvaluating) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + g := b.GetGeneration() + assert.ErrorContains(c, g.EvalErr, "context deadline exceeded") + }, 3*time.Second, 100*time.Millisecond, "builder timeout didn't work") +} diff --git a/internal/builder/exec.go b/internal/builder/exec.go new file mode 100644 index 0000000..a7fc0b5 --- /dev/null +++ b/internal/builder/exec.go @@ -0,0 +1,75 @@ +package builder + +import ( + "context" + "sync" + "time" +) + +type Runnable interface { + Run(c context.Context) error +} + +type Exec struct { + timeout time.Duration + runnable Runnable + Started bool + Finished bool + Stopped bool + Timeouted bool + done chan struct{} + err error + cancelFunc context.CancelFunc + mu sync.Mutex +} + +func NewExec(r Runnable, timeout time.Duration) Exec { + return Exec{ + runnable: r, + mu: sync.Mutex{}, + done: make(chan struct{}), + timeout: timeout, + } +} + +func (e *Exec) Start(ctx context.Context) { + e.mu.Lock() + defer e.mu.Unlock() + e.Started = true + ctx, e.cancelFunc = context.WithCancel(ctx) + ctx, cancel := context.WithTimeout(ctx, e.timeout) + + go func() { + defer e.cancelFunc() + defer cancel() + err := e.runnable.Run(ctx) + e.mu.Lock() + defer e.mu.Unlock() + if ctx.Err() != nil { + e.err = ctx.Err() + } else { + e.err = err + } + e.Started = false + e.Finished = true + select { + case e.done <- struct{}{}: + default: + } + }() +} + +func (e *Exec) Wait() { + if e.Started { + <-e.done + } +} + +func (e *Exec) Stop() { + e.mu.Lock() + defer e.mu.Unlock() + if e.Started { + e.Stopped = true + e.cancelFunc() + } +} diff --git a/internal/builder/exec_test.go b/internal/builder/exec_test.go new file mode 100644 index 0000000..4527120 --- /dev/null +++ b/internal/builder/exec_test.go @@ -0,0 +1,73 @@ +package builder + +import ( + "context" + "fmt" + "os/exec" + "testing" + "time" + + _ "net/http/pprof" + + "github.com/stretchr/testify/assert" +) + +type RunnableDummy struct { + result int +} + +func (r *RunnableDummy) Run(ctx context.Context) error { + r.result = 1 + return nil +} + +func TestNewExec(t *testing.T) { + r := &RunnableDummy{} + e := NewExec(r, time.Second) + assert.Equal(t, 0, r.result) + e.Start(context.TODO()) + e.Wait() + assert.Equal(t, 1, r.result) + assert.True(t, e.Finished) + assert.Nil(t, e.err) +} + +type RunnableContext struct{} + +func (r *RunnableContext) Run(ctx context.Context) error { + cmd := exec.CommandContext(ctx, "sleep", "3") + err := cmd.Run() + return err +} +func TestExecTimeout(t *testing.T) { + r := &RunnableContext{} + e := NewExec(r, time.Second) + e.Start(context.TODO()) + e.Wait() + assert.Equal(t, context.DeadlineExceeded, e.err) +} + +func TestExecStop(t *testing.T) { + r := &RunnableContext{} + e := NewExec(r, 5*time.Second) + e.Start(context.TODO()) + time.Sleep(500 * time.Millisecond) + e.Stop() + e.Wait() + assert.True(t, e.Stopped) + assert.Equal(t, context.Canceled, e.err) +} + +type RunnableError struct{} + +func (r *RunnableError) Run(ctx context.Context) error { + return fmt.Errorf("An error occured") +} +func TestExecError(t *testing.T) { + r := &RunnableError{} + e := NewExec(r, 5*time.Second) + e.Start(context.TODO()) + e.Wait() + assert.True(t, e.Finished) + assert.ErrorContains(t, e.err, "An error occured") +} diff --git a/internal/builder/generation.go b/internal/builder/generation.go new file mode 100644 index 0000000..fdb051c --- /dev/null +++ b/internal/builder/generation.go @@ -0,0 +1,127 @@ +package builder + +import ( + "fmt" + "strings" + "time" + + "github.com/dustin/go-humanize" +) + +type EvalStatus int64 + +const ( + EvalInit EvalStatus = iota + Evaluating + Evaluated + EvalFailed +) + +func (s EvalStatus) String() string { + switch s { + case EvalInit: + return "initialized" + case Evaluating: + return "evaluating" + case Evaluated: + return "evaluated" + case EvalFailed: + return "failed" + } + return "unknown" +} + +type BuildStatus int64 + +const ( + BuildInit BuildStatus = iota + Building + Built + BuildFailed +) + +func (s BuildStatus) String() string { + switch s { + case BuildInit: + return "initialized" + case Building: + return "building" + case Built: + return "built" + case BuildFailed: + return "failed" + } + return "unknown" +} + +// We consider each created genration is legit to be deployed: hard +// reset is ensured at RepositoryStatus creation. +type Generation struct { + UUID string `json:"uuid"` + FlakeUrl string `json:"flake-url"` + Hostname string `json:"hostname"` + + SelectedRemoteUrl string `json:"remote-url"` + SelectedRemoteName string `json:"remote-name"` + SelectedBranchName string `json:"branch-name"` + SelectedCommitId string `json:"commit-id"` + SelectedCommitMsg string `json:"commit-msg"` + SelectedBranchIsTesting bool `json:"branch-is-testing"` + + MainCommitId string `json:"main-commit-id"` + MainRemoteName string `json:"main-remote-name"` + MainBranchName string `json:"main-branch-name"` + + EvalStatus EvalStatus `json:"eval-status"` + EvalStartedAt time.Time `json:"eval-started-at"` + EvalEndedAt time.Time `json:"eval-ended-at"` + EvalErr error `json:"-"` + EvalErrStr string `json:"eval-err"` + OutPath string `json:"outpath"` + DrvPath string `json:"drvpath"` + + MachineId string `json:"machine-id"` + + BuildStatus BuildStatus `json:"build-status"` + BuildStartedAt time.Time `json:"build-started-at"` + BuildEndedAt time.Time `json:"build-ended-at"` + BuildErr error `json:"-"` + BuildErrStr string `json:"build-err"` +} + +func GenerationShow(g Generation) { + padding := " " + fmt.Printf("%sGeneration UUID %s\n", padding, g.UUID) + fmt.Printf("%sCommit ID %s from %s/%s\n", padding, g.SelectedCommitId, g.SelectedRemoteName, g.SelectedBranchName) + fmt.Printf("%sCommit message: %s\n", padding, strings.Trim(g.SelectedCommitMsg, "\n")) + + if g.EvalStatus == EvalInit { + fmt.Printf("%sNo evaluation started\n", padding) + return + } + if g.EvalStatus == Evaluating { + fmt.Printf("%sEvaluation started %s\n", padding, humanize.Time(g.EvalStartedAt)) + return + } + if g.EvalStatus == Evaluated { + fmt.Printf("%sEvaluation succedded %s\n", padding, humanize.Time(g.EvalEndedAt)) + fmt.Printf("%s DrvPath: %s\n", padding, g.DrvPath) + } else if g.EvalStatus == EvalFailed { + fmt.Printf("%sEvaluation failed %s\n", padding, humanize.Time(g.EvalEndedAt)) + } + + if g.BuildStatus == BuildInit { + fmt.Printf("%sNo build started\n", padding) + return + } + if g.BuildStatus == Building { + fmt.Printf("%sBuild started %s\n", padding, humanize.Time(g.BuildStartedAt)) + return + } + if g.BuildStatus == Built { + fmt.Printf("%sBuilt %s\n", padding, humanize.Time(g.BuildEndedAt)) + fmt.Printf("%s Outpath: %s\n", padding, g.OutPath) + } else if g.BuildStatus == BuildFailed { + fmt.Printf("%sBuild failed %s\n", padding, humanize.Time(g.BuildEndedAt)) + } +} diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go new file mode 100644 index 0000000..5f9f939 --- /dev/null +++ b/internal/deployer/deployer.go @@ -0,0 +1,198 @@ +package deployer + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/dustin/go-humanize" + "github.com/google/uuid" + "github.com/nlewo/comin/internal/builder" + "github.com/sirupsen/logrus" +) + +type Status int64 + +const ( + Init Status = iota + Running + Done + Failed +) + +func StatusToString(status Status) string { + switch status { + case Init: + return "init" + case Running: + return "running" + case Done: + return "done" + case Failed: + return "failed" + } + return "" +} + +type Deployment struct { + UUID string `json:"uuid"` + Generation builder.Generation `json:"generation"` + StartedAt time.Time `json:"started_at"` + EndedAt time.Time `json:"ended_at"` + // It is ignored in the JSON marshaling + Err error `json:"-"` + ErrorMsg string `json:"error_msg"` + RestartComin bool `json:"restart_comin"` + ProfilePath string `json:"profile_path"` + Status Status `json:"status"` + Operation string `json:"operation"` +} + +type DeployFunc func(context.Context, string, string) (bool, string, error) + +type Deployer struct { + GenerationCh chan builder.Generation + deployerFunc DeployFunc + DeploymentDoneCh chan Deployment + mu sync.Mutex + Deployment *Deployment + previousDeployment *Deployment + IsDeploying bool + // The next generation to deploy. nil when there is no new generation to deploy + GenerationToDeploy *builder.Generation + // Is there a generation which + generationAvailable bool + generationAvailableCh chan struct{} +} + +func (d Deployment) IsTesting() bool { + return d.Operation == "test" +} + +type State struct { + IsDeploying bool `json:"is_deploying"` + GenerationToDeploy *builder.Generation `json:"generation_to_deploy"` + Deployment *Deployment `json:"deployment"` + PreviousDeployment *Deployment `json:"previous_deployment"` +} + +func (d *Deployer) State() State { + return State{ + IsDeploying: d.IsDeploying, + GenerationToDeploy: d.GenerationToDeploy, + Deployment: d.Deployment, + PreviousDeployment: d.previousDeployment, + } +} + +func showDeployment(padding string, d Deployment) { + switch d.Status { + case Running: + fmt.Printf("%sDeployment is running since %s\n", padding, humanize.Time(d.StartedAt)) + fmt.Printf("%sOperation %s\n", padding, d.Operation) + case Done: + fmt.Printf("%sDeployment succeeded %s\n", padding, humanize.Time(d.EndedAt)) + fmt.Printf("%sOperation %s\n", padding, d.Operation) + fmt.Printf("%sProfilePath %s\n", padding, d.ProfilePath) + case Failed: + fmt.Printf("%sDeployment failed %s\n", padding, humanize.Time(d.EndedAt)) + fmt.Printf("%sOperation %s\n", padding, d.Operation) + fmt.Printf("%sProfilePath %s\n", padding, d.ProfilePath) + } + fmt.Printf("%sGeneration %s\n", padding, d.Generation.UUID) + fmt.Printf("%sCommit ID %s from %s/%s\n", padding, d.Generation.SelectedCommitId, d.Generation.SelectedRemoteName, d.Generation.SelectedBranchName) + fmt.Printf("%sCommit message %s\n", padding, strings.Trim(d.Generation.SelectedCommitMsg, "\n")) + fmt.Printf("%sOutpath %s\n", padding, d.Generation.OutPath) +} + +func (s State) Show(padding string) { + fmt.Printf(" Deployer\n") + if s.Deployment == nil { + showDeployment(padding, *s.PreviousDeployment) + return + } + showDeployment(padding, *s.Deployment) +} + +func New(deployFunc DeployFunc, previousDeployment *Deployment) *Deployer { + return &Deployer{ + DeploymentDoneCh: make(chan Deployment, 1), + deployerFunc: deployFunc, + generationAvailableCh: make(chan struct{}, 1), + previousDeployment: previousDeployment, + } +} + +// Submit submits a generation to be deployed. If a deployment is +// running, this generation will be deployed once the current +// deployment is finished. If this generation is the same than the one +// of the last deployment, this generation is skipped. +func (d *Deployer) Submit(generation builder.Generation) { + logrus.Infof("deployer: submiting generation %s", generation.UUID) + d.mu.Lock() + if d.previousDeployment == nil || generation.SelectedCommitId != d.previousDeployment.Generation.SelectedCommitId || generation.SelectedBranchIsTesting != d.previousDeployment.Generation.SelectedBranchIsTesting { + d.GenerationToDeploy = &generation + select { + case d.generationAvailableCh <- struct{}{}: + default: + } + } else { + logrus.Infof("deployer: skipping deployment of the generation %s because it is the same than the last deployment", generation.UUID) + } + d.mu.Unlock() +} + +func (d *Deployer) Run() { + go func() { + for { + <-d.generationAvailableCh + d.mu.Lock() + g := d.GenerationToDeploy + d.GenerationToDeploy = nil + d.mu.Unlock() + logrus.Infof("deployer: deploying generation %s", g.UUID) + + operation := "switch" + if g.SelectedBranchIsTesting { + operation = "test" + } + dpl := Deployment{ + UUID: uuid.NewString(), + Generation: *g, + Operation: operation, + StartedAt: time.Now().UTC(), + Status: Running, + } + d.mu.Lock() + d.previousDeployment = d.Deployment + d.Deployment = &dpl + d.IsDeploying = true + d.mu.Unlock() + + ctx := context.TODO() + cominNeedRestart, profilePath, err := d.deployerFunc( + ctx, + g.OutPath, + operation, + ) + d.mu.Lock() + d.IsDeploying = false + d.Deployment.EndedAt = time.Now().UTC() + d.Deployment.Err = err + if err != nil { + d.Deployment.ErrorMsg = err.Error() + d.Deployment.Status = Failed + } else { + d.Deployment.Status = Done + } + d.Deployment.RestartComin = cominNeedRestart + d.Deployment.ProfilePath = profilePath + select { + case d.DeploymentDoneCh <- *d.Deployment: + } + d.mu.Unlock() + } + }() +} diff --git a/internal/deployer/deployer_test.go b/internal/deployer/deployer_test.go new file mode 100644 index 0000000..8a82bfa --- /dev/null +++ b/internal/deployer/deployer_test.go @@ -0,0 +1,79 @@ +package deployer + +import ( + "context" + "testing" + "time" + + "github.com/nlewo/comin/internal/builder" + "github.com/stretchr/testify/assert" +) + +func TestDeployerBasic(t *testing.T) { + deployDone := make(chan struct{}) + var deployFunc = func(context.Context, string, string) (bool, string, error) { + <-deployDone + return false, "profile-path", nil + } + + d := New(deployFunc, nil) + d.Run() + assert.False(t, d.IsDeploying) + + g := builder.Generation{SelectedCommitId: "commit-1"} + d.Submit(g) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, d.IsDeploying) + }, 5*time.Second, 100*time.Millisecond) + + deployDone <- struct{}{} + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.False(c, d.IsDeploying) + assert.Equal(c, "profile-path", d.Deployment.ProfilePath) + }, 5*time.Second, 100*time.Millisecond) + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + dpl := <-d.DeploymentDoneCh + assert.Equal(c, "profile-path", dpl.ProfilePath) + assert.Equal(c, "commit-1", dpl.Generation.SelectedCommitId) + }, 5*time.Second, 100*time.Millisecond) + +} + +func TestDeployerSubmit(t *testing.T) { + deployDone := make(chan struct{}) + var deployFunc = func(context.Context, string, string) (bool, string, error) { + <-deployDone + return false, "profile-path", nil + } + + d := New(deployFunc, nil) + d.Run() + assert.False(t, d.IsDeploying) + + d.Submit(builder.Generation{SelectedCommitId: "commit-1"}) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, d.IsDeploying) + assert.Nil(c, d.GenerationToDeploy) + }, 5*time.Second, 100*time.Millisecond) + + d.Submit(builder.Generation{SelectedCommitId: "commit-2"}) + d.Submit(builder.Generation{SelectedCommitId: "commit-3"}) + assert.NotNil(t, d.GenerationToDeploy) + + // To simulate the end of 2 deployments (commit-1 and commit-3) + deployDone <- struct{}{} + deployDone <- struct{}{} + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.False(c, d.IsDeploying) + assert.Equal(c, "profile-path", d.Deployment.ProfilePath) + assert.Nil(t, d.GenerationToDeploy) + }, 5*time.Second, 100*time.Millisecond) + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + dpl := <-d.DeploymentDoneCh + assert.Equal(c, "profile-path", dpl.ProfilePath) + assert.Equal(c, "commit-1", dpl.Generation.SelectedCommitId) + }, 5*time.Second, 100*time.Millisecond) + +} diff --git a/internal/deployment/deployment.go b/internal/deployment/deployment.go deleted file mode 100644 index 9d85cea..0000000 --- a/internal/deployment/deployment.go +++ /dev/null @@ -1,139 +0,0 @@ -package deployment - -import ( - "context" - "time" - - "github.com/google/uuid" - "github.com/nlewo/comin/internal/generation" - "github.com/sirupsen/logrus" -) - -type Status int64 - -const ( - Init Status = iota - Running - Done - Failed -) - -func StatusToString(status Status) string { - switch status { - case Init: - return "init" - case Running: - return "running" - case Done: - return "done" - case Failed: - return "failed" - } - return "" -} - -func StatusFromString(status string) Status { - switch status { - case "init": - return Init - case "running": - return Running - case "done": - return Done - case "failed": - return Failed - } - return Init -} - -type DeployFunc func(context.Context, string, string, string) (bool, string, error) - -type Deployment struct { - UUID string `json:"uuid"` - Generation generation.Generation `json:"generation"` - StartAt time.Time `json:"start_at"` - EndAt time.Time `json:"end_at"` - // It is ignored in the JSON marshaling - Err error `json:"-"` - ErrorMsg string `json:"error_msg"` - RestartComin bool `json:"restart_comin"` - ProfilePath string `json:"profile_path"` - Status Status `json:"status"` - Operation string `json:"operation"` - - deployerFunc DeployFunc - deploymentCh chan DeploymentResult -} - -type DeploymentResult struct { - Err error - EndAt time.Time - RestartComin bool - ProfilePath string -} - -func New(g generation.Generation, deployerFunc DeployFunc, deploymentCh chan DeploymentResult) Deployment { - operation := "switch" - if g.SelectedBranchIsTesting { - operation = "test" - } - - return Deployment{ - UUID: uuid.NewString(), - Generation: g, - deployerFunc: deployerFunc, - deploymentCh: deploymentCh, - Status: Init, - Operation: operation, - } -} - -func (d Deployment) Update(dr DeploymentResult) Deployment { - d.EndAt = dr.EndAt - d.Err = dr.Err - if d.Err != nil { - d.ErrorMsg = dr.Err.Error() - } - d.RestartComin = dr.RestartComin - d.ProfilePath = dr.ProfilePath - if dr.Err == nil { - d.Status = Done - } else { - d.Status = Failed - } - return d -} - -func (d Deployment) IsTesting() bool { - return d.Operation == "test" -} - -// Deploy returns a updated deployment (mainly the startAt is updated) -// and asyncronously tun the deployment. Once finished, a -// DeploymentResult is emitted on the channel d.deploymentCh. -func (d Deployment) Deploy(ctx context.Context) Deployment { - go func() { - // FIXME: propagate context - cominNeedRestart, profilePath, err := d.deployerFunc( - ctx, - d.Generation.EvalMachineId, - d.Generation.OutPath, - d.Operation, - ) - - deploymentResult := DeploymentResult{} - deploymentResult.Err = err - if err != nil { - logrus.Error(err) - logrus.Infof("Deployment failed") - } - - deploymentResult.EndAt = time.Now().UTC() - deploymentResult.RestartComin = cominNeedRestart - deploymentResult.ProfilePath = profilePath - d.deploymentCh <- deploymentResult - }() - d.Status = Running - d.StartAt = time.Now().UTC() - return d -} diff --git a/internal/deployment/deployment_test.go b/internal/deployment/deployment_test.go deleted file mode 100644 index 3c65985..0000000 --- a/internal/deployment/deployment_test.go +++ /dev/null @@ -1 +0,0 @@ -package deployment diff --git a/internal/fetcher/fetcher.go b/internal/fetcher/fetcher.go new file mode 100644 index 0000000..ad17467 --- /dev/null +++ b/internal/fetcher/fetcher.go @@ -0,0 +1,86 @@ +package fetcher + +import ( + "context" + "time" + + "github.com/nlewo/comin/internal/repository" + "github.com/sirupsen/logrus" +) + +type Fetcher struct { + State + submitRemotes chan []string + RepositoryStatusCh chan repository.RepositoryStatus + repo repository.Repository +} + +func NewFetcher(repo repository.Repository) *Fetcher { + return &Fetcher{ + repo: repo, + submitRemotes: make(chan []string), + RepositoryStatusCh: make(chan repository.RepositoryStatus), + } +} + +func (f *Fetcher) TriggerFetch(remotes []string) { + f.submitRemotes <- remotes +} + +type RemoteState struct { + Name string `json:"name"` + FetchedAt time.Time `json:"fetched_at"` +} +type State struct { + IsFetching bool `jsona:"is_fetching"` + RepositoryStatus repository.RepositoryStatus +} + +// FIXME: make it thread safe +func (f *Fetcher) GetState() State { + return State{ + IsFetching: f.IsFetching, + RepositoryStatus: f.RepositoryStatus, + } +} + +func (f *Fetcher) Start() { + logrus.Info("fetcher: starting") + go func() { + remotes := make([]string, 0) + var workerRepositoryStatusCh chan repository.RepositoryStatus + for { + select { + case submittedRemotes := <-f.submitRemotes: + remotes = union(remotes, submittedRemotes) + case rs := <-workerRepositoryStatusCh: + f.IsFetching = false + if rs.SelectedCommitId != f.RepositoryStatus.SelectedCommitId || rs.SelectedBranchIsTesting != f.RepositoryStatus.SelectedBranchIsTesting { + f.RepositoryStatus = rs + f.RepositoryStatusCh <- rs + } + } + if !f.IsFetching && len(remotes) != 0 { + f.IsFetching = true + workerRepositoryStatusCh = f.repo.FetchAndUpdate(context.TODO(), remotes) + remotes = []string{} + } + } + }() +} + +func union(array1, array2 []string) []string { + for _, e2 := range array2 { + exist := false + for _, e1 := range array1 { + if e2 == e1 { + exist = true + break + } + } + if !exist { + array1 = append(array1, e2) + } + } + return array1 +} diff --git a/internal/fetcher/fetcher_test.go b/internal/fetcher/fetcher_test.go new file mode 100644 index 0000000..7911c13 --- /dev/null +++ b/internal/fetcher/fetcher_test.go @@ -0,0 +1,62 @@ +package fetcher + +import ( + "fmt" + "testing" + "time" + + "github.com/nlewo/comin/internal/repository" + "github.com/nlewo/comin/internal/utils" + "github.com/stretchr/testify/assert" +) + +func TestFetcher(t *testing.T) { + r := utils.NewRepositoryMock() + f := NewFetcher(r) + f.Start() + var commitId string + + for i := 0; i < 2; i++ { + assert.False(t, f.IsFetching) + f.TriggerFetch([]string{"remote"}) + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, f.IsFetching) + }, 5*time.Second, 100*time.Millisecond, "fetcher is not fetching") + + // This is to simulate a git fetch + commitId = fmt.Sprintf("id-%d", i) + r.RsCh <- repository.RepositoryStatus{ + SelectedCommitId: commitId, + } + assert.EventuallyWithT(t, func(c *assert.CollectT) { + rs := <-f.RepositoryStatusCh + assert.Equal(c, commitId, rs.SelectedCommitId) + }, 5*time.Second, 100*time.Millisecond, "fetcher failed to fetch") + + assert.False(t, f.IsFetching) + } + + f.TriggerFetch([]string{"remote"}) + r.RsCh <- repository.RepositoryStatus{ + SelectedCommitId: "id-5", + } + assert.EventuallyWithT(t, func(c *assert.CollectT) { + rs := <-f.RepositoryStatusCh + assert.Equal(c, "id-5", rs.SelectedCommitId) + }, 5*time.Second, 100*time.Millisecond, "fetcher failed to fetch") + + r.RsCh <- repository.RepositoryStatus{ + SelectedCommitId: "id-5", + } + r.RsCh <- repository.RepositoryStatus{ + SelectedCommitId: "id-6", + } + rs := <-f.RepositoryStatusCh + assert.NotEqual(t, "id-5", rs.SelectedCommitId) +} + +func TestUnion(t *testing.T) { + res := union([]string{"r1", "r2"}, []string{"r1", "r3"}) + assert.Equal(t, []string{"r1", "r2", "r3"}, res) +} diff --git a/internal/generation/generation.go b/internal/generation/generation.go deleted file mode 100644 index b58e86b..0000000 --- a/internal/generation/generation.go +++ /dev/null @@ -1,230 +0,0 @@ -package generation - -import ( - "context" - "fmt" - "time" - - "github.com/google/uuid" - "github.com/nlewo/comin/internal/repository" - "github.com/sirupsen/logrus" -) - -type Status int64 - -const ( - Init Status = iota - Evaluating - EvaluationSucceeded - EvaluationFailed - Building - BuildSucceeded - BuildFailed -) - -func StatusToString(status Status) string { - switch status { - case Init: - return "init" - case Evaluating: - return "evaluating" - case EvaluationSucceeded: - return "evaluation-succeeded" - case EvaluationFailed: - return "evaluation-failed" - case Building: - return "building" - case BuildSucceeded: - return "build-succeeded" - case BuildFailed: - return "build-failed" - } - return "" -} - -func StatusFromString(status string) Status { - switch status { - case "init": - return Init - case "evaluating": - return Evaluating - case "evaluation-succeeded": - return EvaluationSucceeded - case "evaluation-failed": - return EvaluationFailed - case "building": - return Building - case "build-succeeded": - return BuildSucceeded - case "build-failed": - return BuildFailed - } - return Init -} - -// We consider each created genration is legit to be deployed: hard -// reset is ensured at RepositoryStatus creation. -type Generation struct { - UUID string `json:"uuid"` - FlakeUrl string `json:"flake-url"` - Hostname string `json:"hostname"` - MachineId string `json:"machine-id"` - - Status Status `json:"status"` - - SelectedRemoteUrl string `json:"remote-url"` - SelectedRemoteName string `json:"remote-name"` - SelectedBranchName string `json:"branch-name"` - SelectedCommitId string `json:"commit-id"` - SelectedCommitMsg string `json:"commit-msg"` - SelectedBranchIsTesting bool `json:"branch-is-testing"` - - MainCommitId string `json:"main-commit-id"` - MainRemoteName string `json:"main-remote-name"` - MainBranchName string `json:"main-branch-name"` - - EvalStartedAt time.Time `json:"eval-started-at"` - evalTimeout time.Duration - evalFunc EvalFunc - evalCh chan EvalResult - - EvalEndedAt time.Time `json:"eval-ended-at"` - EvalErr error `json:"-"` - OutPath string `json:"outpath"` - DrvPath string `json:"drvpath"` - EvalMachineId string `json:"eval-machine-id"` - - BuildStartedAt time.Time `json:"build-started-at"` - BuildEndedAt time.Time `json:"build-ended-at"` - buildErr error `json:"-"` - buildFunc BuildFunc - buildCh chan BuildResult -} - -type EvalFunc func(ctx context.Context, flakeUrl string, hostname string) (drvPath string, outPath string, machineId string, err error) -type BuildFunc func(ctx context.Context, drvPath string) error - -type BuildResult struct { - EndAt time.Time - Err error -} - -type EvalResult struct { - EndAt time.Time - OutPath string - DrvPath string - MachineId string - Err error -} - -func New(repositoryStatus repository.RepositoryStatus, flakeUrl, hostname, machineId string, evalFunc EvalFunc, buildFunc BuildFunc) Generation { - remoteUrl := "" - for _, r := range repositoryStatus.Remotes { - if r.Name == repositoryStatus.SelectedRemoteName { - remoteUrl = r.Url - } - } - return Generation{ - UUID: uuid.NewString(), - SelectedRemoteUrl: remoteUrl, - SelectedRemoteName: repositoryStatus.SelectedRemoteName, - SelectedBranchName: repositoryStatus.SelectedBranchName, - SelectedCommitId: repositoryStatus.SelectedCommitId, - SelectedCommitMsg: repositoryStatus.SelectedCommitMsg, - SelectedBranchIsTesting: repositoryStatus.SelectedBranchIsTesting, - - MainRemoteName: repositoryStatus.MainRemoteName, - MainBranchName: repositoryStatus.MainBranchName, - MainCommitId: repositoryStatus.MainCommitId, - - evalTimeout: 6 * time.Second, - evalFunc: evalFunc, - buildFunc: buildFunc, - FlakeUrl: flakeUrl, - Hostname: hostname, - MachineId: machineId, - Status: Init, - } -} - -func (g Generation) EvalCh() chan EvalResult { - return g.evalCh -} - -func (g Generation) BuildCh() chan BuildResult { - return g.buildCh -} - -func (g Generation) UpdateEval(r EvalResult) Generation { - logrus.Debugf("Eval done with %#v", r) - g.EvalEndedAt = r.EndAt - g.DrvPath = r.DrvPath - g.OutPath = r.OutPath - g.EvalMachineId = r.MachineId - g.EvalErr = r.Err - if g.EvalErr == nil { - g.Status = EvaluationSucceeded - } else { - g.Status = EvaluationFailed - } - return g -} - -func (g Generation) UpdateBuild(r BuildResult) Generation { - logrus.Debugf("Build done with %#v", r) - g.BuildEndedAt = r.EndAt - g.buildErr = r.Err - if g.buildErr == nil { - g.Status = BuildSucceeded - } else { - g.Status = BuildFailed - } - return g -} - -func (g Generation) Eval(ctx context.Context) Generation { - g.evalCh = make(chan EvalResult) - g.EvalStartedAt = time.Now().UTC() - g.Status = Evaluating - - fn := func() { - ctx, cancel := context.WithTimeout(ctx, g.evalTimeout) - defer cancel() - drvPath, outPath, machineId, err := g.evalFunc(ctx, g.FlakeUrl, g.Hostname) - evaluationResult := EvalResult{ - EndAt: time.Now().UTC(), - } - if err == nil { - evaluationResult.DrvPath = drvPath - evaluationResult.OutPath = outPath - evaluationResult.MachineId = machineId - if machineId != "" && g.MachineId != machineId { - evaluationResult.Err = fmt.Errorf("The evaluated comin.machineId '%s' is different from the /etc/machine-id '%s' of this machine", - machineId, g.MachineId) - } - } else { - evaluationResult.Err = err - } - g.evalCh <- evaluationResult - } - go fn() - return g -} - -func (g Generation) Build(ctx context.Context) Generation { - g.buildCh = make(chan BuildResult) - g.BuildStartedAt = time.Now().UTC() - g.Status = Building - fn := func() { - ctx, cancel := context.WithTimeout(ctx, g.evalTimeout) - defer cancel() - err := g.buildFunc(ctx, g.DrvPath) - buildResult := BuildResult{ - EndAt: time.Now().UTC(), - } - buildResult.Err = err - g.buildCh <- buildResult - } - go fn() - return g -} diff --git a/internal/generation/generation_test.go b/internal/generation/generation_test.go deleted file mode 100644 index 40b73de..0000000 --- a/internal/generation/generation_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package generation - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/nlewo/comin/internal/repository" - "github.com/stretchr/testify/assert" -) - -func TestEval(t *testing.T) { - var evalResult EvalResult - var ctx context.Context - machineId := "machine-id" - evalDone := make(chan struct{}) - - nixEvalMock := func(ctx context.Context, repositoryPath string, hostname string) (string, string, string, error) { - select { - case <-ctx.Done(): - return "", "", "", fmt.Errorf("timeout exceeded") - case <-evalDone: - return "", "", machineId, nil - } - } - nixBuildMock := func(ctx context.Context, drv string) error { - return nil - } - - repositoryPath := "repository/path/" - hostname := "machine" - g := New(repository.RepositoryStatus{}, repositoryPath, hostname, machineId, nixEvalMock, nixBuildMock) - g.evalTimeout = 1 * time.Second - - // The eval job never terminates so it should timeout - ctx = context.Background() - g = g.Eval(ctx) - evalResult = <-g.EvalCh() - assert.NotNil(t, evalResult.Err) - assert.EqualError(t, evalResult.Err, "timeout exceeded") - - ctx = context.Background() - g = g.Eval(ctx) - // This is to simulate the eval completion - close(evalDone) - evalResult = <-g.EvalCh() - assert.Nil(t, evalResult.Err) - assert.Equal(t, machineId, evalResult.MachineId) -} diff --git a/internal/http/http.go b/internal/http/http.go index 7c7cfba..c0b2de2 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -12,7 +12,7 @@ import ( "github.com/sirupsen/logrus" ) -func handlerStatus(m manager.Manager, w http.ResponseWriter, r *http.Request) { +func handlerStatus(m *manager.Manager, w http.ResponseWriter, r *http.Request) { logrus.Infof("Getting status request %s from %s", r.URL, r.RemoteAddr) w.WriteHeader(http.StatusOK) s := m.GetState() @@ -28,21 +28,43 @@ func handlerStatus(m manager.Manager, w http.ResponseWriter, r *http.Request) { // Serve starts http servers. We create two HTTP servers to easily be // able to expose metrics publicly while keeping on localhost only the // API. -func Serve(m manager.Manager, p prometheus.Prometheus, apiAddress string, apiPort int, metricsAddress string, metricsPort int) { +func Serve(m *manager.Manager, p prometheus.Prometheus, apiAddress string, apiPort int, metricsAddress string, metricsPort int) { handlerStatusFn := func(w http.ResponseWriter, r *http.Request) { handlerStatus(m, w, r) return } + handlerFetcherFn := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + s := m.GetState().Fetcher + rJson, _ := json.MarshalIndent(s, "", "\t") + io.WriteString(w, string(rJson)) + } + handlerFetcherFetchFn := func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + s := m.GetState().Fetcher + remotes := make([]string, 0) + for _, r := range s.RepositoryStatus.Remotes { + remotes = append(remotes, r.Name) + } + m.Fetcher.TriggerFetch(remotes) + w.WriteHeader(http.StatusOK) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + } - muxStatus := http.NewServeMux() - muxStatus.HandleFunc("/status", handlerStatusFn) + muxApi := http.NewServeMux() + muxApi.HandleFunc("/api/status", handlerStatusFn) + muxApi.HandleFunc("/api/fetcher", handlerFetcherFn) + muxApi.HandleFunc("/api/fetcher/fetch", handlerFetcherFetchFn) muxMetrics := http.NewServeMux() muxMetrics.Handle("/metrics", p.Handler()) go func() { url := fmt.Sprintf("%s:%d", apiAddress, apiPort) logrus.Infof("Starting the API server on %s", url) - if err := http.ListenAndServe(url, muxStatus); err != nil { + if err := http.ListenAndServe(url, muxApi); err != nil { logrus.Errorf("Error while running the API server: %s", err) os.Exit(1) } diff --git a/internal/manager/manager.go b/internal/manager/manager.go index b48974a..0036433 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -1,248 +1,132 @@ package manager import ( - "context" - "fmt" - - "github.com/nlewo/comin/internal/deployment" - "github.com/nlewo/comin/internal/generation" - "github.com/nlewo/comin/internal/nix" + "github.com/nlewo/comin/internal/builder" + "github.com/nlewo/comin/internal/deployer" + "github.com/nlewo/comin/internal/fetcher" "github.com/nlewo/comin/internal/profile" "github.com/nlewo/comin/internal/prometheus" - "github.com/nlewo/comin/internal/repository" + "github.com/nlewo/comin/internal/scheduler" "github.com/nlewo/comin/internal/store" "github.com/nlewo/comin/internal/utils" "github.com/sirupsen/logrus" ) type State struct { - RepositoryStatus repository.RepositoryStatus `json:"repository_status"` - Generation generation.Generation - IsFetching bool `json:"is_fetching"` - IsRunning bool `json:"is_running"` - Deployment deployment.Deployment `json:"deployment"` - Hostname string `json:"hostname"` - NeedToReboot bool `json:"need_to_reboot"` + NeedToReboot bool `json:"need_to_reboot"` + Fetcher fetcher.State `json:"fetcher"` + Builder builder.State `json:"builder"` + Deployer deployer.State `json:"deployer"` } type Manager struct { - repository repository.Repository - repositoryDir string - // FIXME: a generation should get a repository URL from the repository status - repositoryPath string - hostname string - // The machine id of the current host - machineId string - triggerRepository chan []string - generationFactory func(repository.RepositoryStatus, string, string) generation.Generation - stateRequestCh chan struct{} - stateResultCh chan State - repositoryStatus repository.RepositoryStatus - // The generation currently managed - generation generation.Generation - isFetching bool - // FIXME: this is temporary in order to simplify the manager - // for a first iteration: this needs to be removed - isRunning bool - needToBeRestarted bool - needToReboot bool - cominServiceRestartFunc func() error + // The machine id of the current host. It is used to ensure + // the optionnal machine-id found at evaluation time + // corresponds to the machine-id of this host. + machineId string - evalFunc generation.EvalFunc - buildFunc generation.BuildFunc + stateRequestCh chan struct{} + stateResultCh chan State - deploymentResultCh chan deployment.DeploymentResult - // The deployment currenly managed - deployment deployment.Deployment - deployerFunc deployment.DeployFunc - - repositoryStatusCh chan repository.RepositoryStatus - triggerDeploymentCh chan generation.Generation + needToReboot bool + cominServiceRestartFunc func() error prometheus prometheus.Prometheus storage store.Store + scheduler scheduler.Scheduler + Fetcher *fetcher.Fetcher + builder *builder.Builder + deployer *deployer.Deployer } -func New(r repository.Repository, s store.Store, p prometheus.Prometheus, path, dir, hostname, machineId string) Manager { - m := Manager{ - repository: r, - repositoryDir: dir, - repositoryPath: path, - hostname: hostname, +func New(s store.Store, p prometheus.Prometheus, sched scheduler.Scheduler, fetcher *fetcher.Fetcher, builder *builder.Builder, deployer *deployer.Deployer, machineId string) *Manager { + m := &Manager{ machineId: machineId, - evalFunc: nix.Eval, - buildFunc: nix.Build, - deployerFunc: nix.Deploy, - triggerRepository: make(chan []string), stateRequestCh: make(chan struct{}), stateResultCh: make(chan State), cominServiceRestartFunc: utils.CominServiceRestart, - deploymentResultCh: make(chan deployment.DeploymentResult), - repositoryStatusCh: make(chan repository.RepositoryStatus), - triggerDeploymentCh: make(chan generation.Generation, 1), prometheus: p, storage: s, - } - if len(s.DeploymentList()) > 0 { - d := s.DeploymentList()[0] - logrus.Infof("Restoring the manager state from the last deployment %s", d.UUID) - m.deployment = d - m.generation = d.Generation + scheduler: sched, + Fetcher: fetcher, + builder: builder, + deployer: deployer, } return m } -func (m Manager) GetState() State { +func (m *Manager) GetState() State { m.stateRequestCh <- struct{}{} return <-m.stateResultCh } -func (m Manager) Fetch(remotes []string) { - m.triggerRepository <- remotes -} - -func (m Manager) toState() State { +func (m *Manager) toState() State { return State{ - Generation: m.generation, - RepositoryStatus: m.repositoryStatus, - IsFetching: m.isFetching, - IsRunning: m.isRunning, - Deployment: m.deployment, - Hostname: m.hostname, - NeedToReboot: m.needToReboot, - } -} - -func (m Manager) onEvaluated(ctx context.Context, evalResult generation.EvalResult) Manager { - m.generation = m.generation.UpdateEval(evalResult) - if evalResult.Err == nil { - m.generation = m.generation.Build(ctx) - } else { - m.isRunning = false - } - return m -} - -func (m Manager) onBuilt(ctx context.Context, buildResult generation.BuildResult) Manager { - m.generation = m.generation.UpdateBuild(buildResult) - if buildResult.Err == nil { - m.triggerDeployment(ctx, m.generation) - } else { - m.isRunning = false + NeedToReboot: m.needToReboot, + Fetcher: m.Fetcher.GetState(), + Builder: m.builder.State(), + Deployer: m.deployer.State(), } - return m -} - -func (m Manager) triggerDeployment(ctx context.Context, g generation.Generation) { - m.triggerDeploymentCh <- g -} - -func (m Manager) onTriggerDeployment(ctx context.Context, g generation.Generation) Manager { - m.deployment = deployment.New(g, m.deployerFunc, m.deploymentResultCh) - m.deployment = m.deployment.Deploy(ctx) - return m } -func (m Manager) onDeployment(ctx context.Context, deploymentResult deployment.DeploymentResult) Manager { - logrus.Debugf("Deploy done with %#v", deploymentResult) - m.deployment = m.deployment.Update(deploymentResult) - // The comin service is not restart by the switch-to-configuration script in order to let comin terminating properly. Instead, comin restarts itself. - if m.deployment.RestartComin { - m.needToBeRestarted = true - } - m.isRunning = false - m.prometheus.SetDeploymentInfo(m.deployment.Generation.SelectedCommitId, deployment.StatusToString(m.deployment.Status)) - getsEvicted, evicted := m.storage.DeploymentInsertAndCommit(m.deployment) - if getsEvicted && evicted.ProfilePath != "" { - profile.RemoveProfilePath(evicted.ProfilePath) - } - m.needToReboot = utils.NeedToReboot() - m.prometheus.SetHostInfo(m.needToReboot) - return m -} - -func (m Manager) onRepositoryStatus(ctx context.Context, rs repository.RepositoryStatus) Manager { - logrus.Debugf("Fetch done with %#v", rs) - m.isFetching = false - m.repositoryStatus = rs - - for _, r := range rs.Remotes { - if r.LastFetched { - status := "failed" - if r.FetchErrorMsg == "" { - status = "succeeded" +// FetchAndBuild fetches new commits. If a new commit is available, it +// evaluates and builds the derivation. Once built, it pushes the +// generation on a channel which is consumed by the deployer. +func (m *Manager) FetchAndBuild() { + go func() { + for { + select { + case rs := <-m.Fetcher.RepositoryStatusCh: + logrus.Infof("manager: a generation is evaluating for commit %s", rs.SelectedCommitId) + m.builder.Eval(rs) + case generation := <-m.builder.EvaluationDone: + if generation.EvalErr != nil { + continue + } + if generation.MachineId != "" && m.machineId != generation.MachineId { + logrus.Infof("manager: the comin.machineId %s is not the host machine-id %s", generation.MachineId, m.machineId) + } else { + logrus.Infof("manager: a generation is building for commit %s", generation.SelectedCommitId) + m.builder.Build() + } + case generation := <-m.builder.BuildDone: + if generation.BuildErr == nil { + logrus.Infof("manager: a generation is available for deployment with commit %s", generation.SelectedCommitId) + m.deployer.Submit(generation) + } } - m.prometheus.IncFetchCounter(r.Name, status) } - } - if rs.SelectedCommitId == "" { - logrus.Debugf("No commit has been selected from remotes") - m.isRunning = false - } else if rs.SelectedCommitId == m.generation.SelectedCommitId && rs.SelectedBranchIsTesting == m.generation.SelectedBranchIsTesting { - logrus.Debugf("The repository status is the same than the previous one") - m.isRunning = false - } else { - // g.Stop(): this is required once we remove m.IsRunning - flakeUrl := fmt.Sprintf("git+file://%s?dir=%s&rev=%s", m.repositoryPath, m.repositoryDir, m.repositoryStatus.SelectedCommitId) - m.generation = generation.New(rs, flakeUrl, m.hostname, m.machineId, m.evalFunc, m.buildFunc) - m.generation = m.generation.Eval(ctx) - } - return m + }() } -func (m Manager) onTriggerRepository(ctx context.Context, remoteNames []string) Manager { - if m.isFetching { - logrus.Debugf("The manager is already fetching the repository") - return m - } - // FIXME: we will remove this in future versions - if m.isRunning { - logrus.Debugf("The manager is already running: it is currently not able to run tasks in parallel") - return m - } - logrus.Debugf("Trigger fetch and update remotes %s", remoteNames) - m.isRunning = true - m.isFetching = true - m.repositoryStatusCh = m.repository.FetchAndUpdate(ctx, remoteNames) - return m -} - -func (m Manager) Run() { - ctx := context.TODO() - - logrus.Info("The manager is started") - logrus.Infof(" hostname = %s", m.hostname) - logrus.Infof(" machineId = %s", m.machineId) - logrus.Infof(" repositoryPath = %s", m.repositoryPath) - +func (m *Manager) Run() { + logrus.Infof("manager: starting with machineId=%s", m.machineId) m.needToReboot = utils.NeedToReboot() m.prometheus.SetHostInfo(m.needToReboot) + m.FetchAndBuild() + m.deployer.Run() + for { select { case <-m.stateRequestCh: m.stateResultCh <- m.toState() - case remoteNames := <-m.triggerRepository: - m = m.onTriggerRepository(ctx, remoteNames) - case rs := <-m.repositoryStatusCh: - m = m.onRepositoryStatus(ctx, rs) - case evalResult := <-m.generation.EvalCh(): - m = m.onEvaluated(ctx, evalResult) - case buildResult := <-m.generation.BuildCh(): - m = m.onBuilt(ctx, buildResult) - case generation := <-m.triggerDeploymentCh: - m = m.onTriggerDeployment(ctx, generation) - case deploymentResult := <-m.deploymentResultCh: - m = m.onDeployment(ctx, deploymentResult) - } - if m.needToBeRestarted { - // TODO: stop contexts - if err := m.cominServiceRestartFunc(); err != nil { - logrus.Fatal(err) - return + case dpl := <-m.deployer.DeploymentDoneCh: + m.prometheus.SetDeploymentInfo(dpl.Generation.SelectedCommitId, deployer.StatusToString(dpl.Status)) + getsEvicted, evicted := m.storage.DeploymentInsertAndCommit(dpl) + if getsEvicted && evicted.ProfilePath != "" { + profile.RemoveProfilePath(evicted.ProfilePath) + } + m.needToReboot = utils.NeedToReboot() + m.prometheus.SetHostInfo(m.needToReboot) + if dpl.RestartComin { + // TODO: stop contexts + if err := m.cominServiceRestartFunc(); err != nil { + logrus.Fatal(err) + return + } } - m.needToBeRestarted = false } } } diff --git a/internal/manager/manager_test.go b/internal/manager/manager_test.go index 8bed342..05e737f 100644 --- a/internal/manager/manager_test.go +++ b/internal/manager/manager_test.go @@ -2,13 +2,18 @@ package manager import ( "context" + "fmt" "testing" "time" - "github.com/nlewo/comin/internal/deployment" + "github.com/nlewo/comin/internal/builder" + "github.com/nlewo/comin/internal/deployer" + "github.com/nlewo/comin/internal/fetcher" "github.com/nlewo/comin/internal/prometheus" "github.com/nlewo/comin/internal/repository" + "github.com/nlewo/comin/internal/scheduler" "github.com/nlewo/comin/internal/store" + "github.com/nlewo/comin/internal/utils" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) @@ -17,184 +22,234 @@ type metricsMock struct{} func (m metricsMock) SetDeploymentInfo(commitId, status string) {} -type repositoryMock struct { - rsCh chan repository.RepositoryStatus +var mkNixEvalMock = func(evalOk chan bool) builder.EvalFunc { + return func(ctx context.Context, repositoryPath string, hostname string) (string, string, string, error) { + ok := <-evalOk + if ok { + return "drv-path", "out-path", "", nil + } else { + return "", "", "", fmt.Errorf("An error occured") + } + } } -func newRepositoryMock() (r *repositoryMock) { - rsCh := make(chan repository.RepositoryStatus) - return &repositoryMock{ - rsCh: rsCh, +var mkDeployerMock = func() *deployer.Deployer { + var deployFunc = func(context.Context, string, string) (bool, string, error) { + return false, "", nil } + return deployer.New(deployFunc, nil) } -func (r *repositoryMock) FetchAndUpdate(ctx context.Context, remoteNames []string) (rsCh chan repository.RepositoryStatus) { - return r.rsCh + +var mkNixBuildMock = func(buildOk chan bool) builder.BuildFunc { + return func(ctx context.Context, drvPath string) error { + select { + case <-ctx.Done(): + return ctx.Err() + case ok := <-buildOk: + if ok { + return nil + } else { + return fmt.Errorf("An error occured") + } + } + } } -func TestRun(t *testing.T) { +func TestBuild(t *testing.T) { + evalOk := make(chan bool) + buildOk := make(chan bool) logrus.SetLevel(logrus.DebugLevel) - r := newRepositoryMock() - m := New(r, store.New("", 1, 1), prometheus.New(), "", "", "", "") - - evalDone := make(chan struct{}) - buildDone := make(chan struct{}) - nixEvalMock := func(ctx context.Context, repositoryPath string, hostname string) (string, string, string, error) { - <-evalDone - return "drv-path", "out-path", "", nil + r := utils.NewRepositoryMock() + f := fetcher.NewFetcher(r) + f.Start() + b := builder.New("repoPath", "", "my-machine", 2*time.Second, mkNixEvalMock(evalOk), 2*time.Second, mkNixBuildMock(buildOk)) + var deployFunc = func(context.Context, string, string) (bool, string, error) { + return false, "profile-path", nil } - nixBuildMock := func(ctx context.Context, drvPath string) error { - <-buildDone - return nil + d := deployer.New(deployFunc, nil) + m := New(store.New("", 1, 1), prometheus.New(), scheduler.New(), f, b, d, "") + go m.Run() + assert.False(t, m.Fetcher.GetState().IsFetching) + assert.False(t, m.builder.State().IsEvaluating) + assert.False(t, m.builder.State().IsBuilding) + + commitId := "id-1" + f.TriggerFetch([]string{"remote"}) + r.RsCh <- repository.RepositoryStatus{ + SelectedCommitId: commitId, } - m.evalFunc = nixEvalMock - m.buildFunc = nixBuildMock + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, m.builder.State().IsEvaluating) + assert.False(c, m.builder.State().IsBuilding) + }, 5*time.Second, 100*time.Millisecond) - deployFunc := func(context.Context, string, string, string) (bool, string, error) { - return false, "", nil + // This simulates the failure of an evaluation + evalOk <- false + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.False(c, m.builder.State().IsEvaluating) + assert.False(c, m.builder.State().IsBuilding) + assert.NotNil(c, m.builder.GetGeneration().EvalErr) + assert.Nil(c, m.deployer.GenerationToDeploy) + }, 5*time.Second, 100*time.Millisecond) + + commitId = "id-2" + f.TriggerFetch([]string{"remote"}) + r.RsCh <- repository.RepositoryStatus{ + SelectedCommitId: commitId, } - m.deployerFunc = deployFunc - - go m.Run() - - // the state is empty - assert.Equal(t, State{}, m.GetState()) - - // the repository is fetched - m.Fetch([]string{"origin"}) - assert.Equal(t, repository.RepositoryStatus{}, m.GetState().RepositoryStatus) - - // we inject a repositoryStatus - r.rsCh <- repository.RepositoryStatus{ - SelectedCommitId: "foo", + // This simulates the success of an evaluation + evalOk <- true + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.False(c, m.builder.State().IsEvaluating) + assert.True(c, m.builder.State().IsBuilding) + assert.Nil(c, m.builder.GetGeneration().EvalErr) + assert.Nil(c, m.deployer.GenerationToDeploy) + }, 5*time.Second, 100*time.Millisecond) + + // This simulates the failure of a build + buildOk <- false + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.False(c, m.builder.State().IsEvaluating) + assert.False(c, m.builder.State().IsBuilding) + assert.NotNil(c, m.builder.GetGeneration().BuildErr) + assert.Nil(c, m.deployer.GenerationToDeploy) + }, 5*time.Second, 100*time.Millisecond) + + // This simulates the success of a build + f.TriggerFetch([]string{"remote"}) + r.RsCh <- repository.RepositoryStatus{ + SelectedCommitId: "id-3", } - assert.Equal( - t, - repository.RepositoryStatus{SelectedCommitId: "foo"}, - m.GetState().RepositoryStatus) - - // we simulate the end of the evaluation - close(evalDone) + evalOk <- true + buildOk <- true assert.EventuallyWithT(t, func(c *assert.CollectT) { - assert.Equal(c, "drv-path", m.GetState().Generation.DrvPath) - assert.NotEmpty(c, m.GetState().Generation.EvalEndedAt) - }, 5*time.Second, 100*time.Millisecond, "evaluation is not finished") - - // we simulate the end of the build - close(buildDone) + assert.False(c, m.builder.State().IsEvaluating) + assert.False(c, m.builder.State().IsBuilding) + assert.Nil(c, m.builder.GetGeneration().BuildErr) + }, 5*time.Second, 100*time.Millisecond) + + // This simulates the success of another build and ensure this + // new build is the one proposed for deployment. + f.TriggerFetch([]string{"remote"}) + r.RsCh <- repository.RepositoryStatus{ + SelectedCommitId: "id-4", + } + evalOk <- true + buildOk <- true assert.EventuallyWithT(t, func(c *assert.CollectT) { - assert.NotEmpty(c, m.GetState().Generation.BuildEndedAt) - }, 5*time.Second, 100*time.Millisecond, "build is not finished") - - // we simulate the end of the deploy + assert.False(c, m.builder.State().IsEvaluating) + assert.False(c, m.builder.State().IsBuilding) + assert.Nil(c, m.builder.GetGeneration().BuildErr) + }, 5*time.Second, 100*time.Millisecond) + + // This simulates the push of new commit while building + f.TriggerFetch([]string{"remote"}) + r.RsCh <- repository.RepositoryStatus{ + SelectedCommitId: "id-5", + } + evalOk <- true assert.EventuallyWithT(t, func(c *assert.CollectT) { - assert.NotEmpty(c, m.GetState().Deployment.EndAt) - }, 5*time.Second, 100*time.Millisecond, "deployment is not finished") - + assert.True(c, m.builder.State().IsBuilding) + }, 5*time.Second, 100*time.Millisecond) } -func TestFetchBusy(t *testing.T) { +func TestDeploy(t *testing.T) { + evalOk := make(chan bool) + buildOk := make(chan bool) logrus.SetLevel(logrus.DebugLevel) - r := newRepositoryMock() - m := New(r, store.New("", 1, 1), prometheus.New(), "", "", "", "machine-id") + r := utils.NewRepositoryMock() + f := fetcher.NewFetcher(r) + f.Start() + b := builder.New("repoPath", "", "my-machine", 2*time.Second, mkNixEvalMock(evalOk), 2*time.Second, mkNixBuildMock(buildOk)) + var deployFunc = func(context.Context, string, string) (bool, string, error) { + return false, "profile-path", nil + } + d := deployer.New(deployFunc, nil) + m := New(store.New("", 1, 1), prometheus.New(), scheduler.New(), f, b, d, "") go m.Run() + assert.False(t, m.Fetcher.GetState().IsFetching) + assert.False(t, m.builder.State().IsEvaluating) + assert.False(t, m.builder.State().IsBuilding) - assert.Equal(t, State{}, m.GetState()) - - m.Fetch([]string{"origin"}) - assert.Equal(t, repository.RepositoryStatus{}, m.GetState().RepositoryStatus) + m.deployer.Submit(builder.Generation{}) + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.Equal(c, "profile-path", m.deployer.State().Deployment.ProfilePath) + }, 5*time.Second, 100*time.Millisecond) - m.Fetch([]string{"origin"}) - assert.Equal(t, repository.RepositoryStatus{}, m.GetState().RepositoryStatus) } func TestRestartComin(t *testing.T) { + evalOk := make(chan bool) + buildOk := make(chan bool) logrus.SetLevel(logrus.DebugLevel) - r := newRepositoryMock() - m := New(r, store.New("", 1, 1), prometheus.New(), "", "", "", "machine-id") - dCh := make(chan deployment.DeploymentResult) - m.deploymentResultCh = dCh + r := utils.NewRepositoryMock() + f := fetcher.NewFetcher(r) + f.Start() + b := builder.New("repoPath", "", "my-machine", 2*time.Second, mkNixEvalMock(evalOk), 2*time.Second, mkNixBuildMock(buildOk)) + var deployFunc = func(context.Context, string, string) (bool, string, error) { + return true, "profile-path", nil + } + d := deployer.New(deployFunc, nil) + m := New(store.New("", 1, 1), prometheus.New(), scheduler.New(), f, b, d, "") + go m.Run() + isCominRestarted := false cominServiceRestartMock := func() error { isCominRestarted = true return nil } m.cominServiceRestartFunc = cominServiceRestartMock - go m.Run() - m.deploymentResultCh <- deployment.DeploymentResult{ - RestartComin: true, - } + + m.deployer.Submit(builder.Generation{}) assert.EventuallyWithT(t, func(c *assert.CollectT) { assert.True(c, isCominRestarted) }, 5*time.Second, 100*time.Millisecond, "comin has not been restarted yet") - } -func TestOptionnalMachineId(t *testing.T) { +func TestIncorrectMachineId(t *testing.T) { + buildOk := make(chan bool) logrus.SetLevel(logrus.DebugLevel) - r := newRepositoryMock() - m := New(r, store.New("", 1, 1), prometheus.New(), "", "", "", "the-test-machine-id") - - evalDone := make(chan struct{}) - buildDone := make(chan struct{}) - nixEvalMock := func(ctx context.Context, repositoryPath string, hostname string) (string, string, string, error) { - <-evalDone - // When comin.machineId is empty, comin evaluates it as an empty string - evaluatedMachineId := "" - return "drv-path", "out-path", evaluatedMachineId, nil - } - nixBuildMock := func(ctx context.Context, drvPath string) error { - <-buildDone - return nil + r := utils.NewRepositoryMock() + f := fetcher.NewFetcher(r) + f.Start() + nixEval := func(ctx context.Context, repositoryPath string, hostname string) (string, string, string, error) { + return "drv-path", "out-path", "invalid-machine-id", nil } - m.evalFunc = nixEvalMock - m.buildFunc = nixBuildMock - + b := builder.New("repoPath", "", "my-machine", 2*time.Second, nixEval, 2*time.Second, mkNixBuildMock(buildOk)) + d := mkDeployerMock() + m := New(store.New("", 1, 1), prometheus.New(), scheduler.New(), f, b, d, "the-test-machine-id") go m.Run() - m.Fetch([]string{"origin"}) - r.rsCh <- repository.RepositoryStatus{SelectedCommitId: "foo"} - // we simulate the end of the evaluation - close(evalDone) + f.TriggerFetch([]string{"remote"}) + r.RsCh <- repository.RepositoryStatus{ + SelectedCommitId: "id", + } assert.EventuallyWithT(t, func(c *assert.CollectT) { - assert.True(t, m.GetState().IsRunning) - }, 5*time.Second, 100*time.Millisecond, "evaluation is not finished") + assert.False(t, m.GetState().Builder.IsBuilding) + }, 5*time.Second, 100*time.Millisecond) } -func TestIncorrectMachineId(t *testing.T) { +func TestCorrectMachineId(t *testing.T) { + buildOk := make(chan bool) logrus.SetLevel(logrus.DebugLevel) - r := newRepositoryMock() - m := New(r, store.New("", 1, 1), prometheus.New(), "", "", "", "the-test-machine-id") - - evalDone := make(chan struct{}) - buildDone := make(chan struct{}) - nixEvalMock := func(ctx context.Context, repositoryPath string, hostname string) (string, string, string, error) { - <-evalDone - return "drv-path", "out-path", "incorrect-machine-id", nil - } - nixBuildMock := func(ctx context.Context, drvPath string) error { - <-buildDone - return nil + r := utils.NewRepositoryMock() + f := fetcher.NewFetcher(r) + f.Start() + nixEval := func(ctx context.Context, repositoryPath string, hostname string) (string, string, string, error) { + return "drv-path", "out-path", "the-test-machine-id", nil } - m.evalFunc = nixEvalMock - m.buildFunc = nixBuildMock - + b := builder.New("repoPath", "", "my-machine", 2*time.Second, nixEval, 2*time.Second, mkNixBuildMock(buildOk)) + d := mkDeployerMock() + m := New(store.New("", 1, 1), prometheus.New(), scheduler.New(), f, b, d, "the-test-machine-id") go m.Run() - // the state is empty - assert.Equal(t, State{}, m.GetState()) - - // the repository is fetched - m.Fetch([]string{"origin"}) - r.rsCh <- repository.RepositoryStatus{SelectedCommitId: "foo"} - - assert.True(t, m.GetState().IsRunning) - - // we simulate the end of the evaluation - close(evalDone) + f.TriggerFetch([]string{"remote"}) + r.RsCh <- repository.RepositoryStatus{ + SelectedCommitId: "id", + } assert.EventuallyWithT(t, func(c *assert.CollectT) { - // The manager is no longer running since the machine id are not identical - assert.False(t, m.GetState().IsRunning) - }, 5*time.Second, 100*time.Millisecond, "evaluation is not finished") + assert.True(t, m.GetState().Builder.IsBuilding) + }, 5*time.Second, 100*time.Millisecond) } diff --git a/internal/nix/nix.go b/internal/nix/nix.go index e9ca918..c8d6230 100644 --- a/internal/nix/nix.go +++ b/internal/nix/nix.go @@ -37,10 +37,10 @@ func getExpectedMachineId(path, hostname string) (machineId string, err error) { return } if machineIdPtr != nil { - logrus.Debugf("Getting comin.machineId = %s", *machineIdPtr) + logrus.Debugf("nix: getting comin.machineId = %s", *machineIdPtr) machineId = *machineIdPtr } else { - logrus.Debugf("Getting comin.machineId = null (not set)") + logrus.Debugf("nix: getting comin.machineId = null (not set)") machineId = "" } return @@ -187,7 +187,9 @@ func switchToConfiguration(operation string, outPath string, dryRun bool) error return nil } -func Deploy(ctx context.Context, expectedMachineId, outPath, operation string) (needToRestartComin bool, profilePath string, err error) { +func Deploy(ctx context.Context, outPath, operation string) (needToRestartComin bool, profilePath string, err error) { + // FIXME: this check doesn't have to be here. It should be + // done by the manager. beforeCominUnitFileHash := cominUnitFileHash() // This is required to write boot entries diff --git a/internal/poller/poller.go b/internal/poller/poller.go deleted file mode 100644 index 9b273aa..0000000 --- a/internal/poller/poller.go +++ /dev/null @@ -1,36 +0,0 @@ -package poller - -import ( - "time" - - "github.com/nlewo/comin/internal/manager" - "github.com/nlewo/comin/internal/types" - "github.com/sirupsen/logrus" -) - -func Poller(m manager.Manager, remotes []types.Remote) { - poll := false - for _, remote := range remotes { - if remote.Poller.Period != 0 { - logrus.Infof("Starting the poller for the remote '%s' with period %ds", remote.Name, remote.Poller.Period) - poll = true - } - } - if !poll { - return - } - counter := 0 - for { - toFetch := make([]string, 0) - for _, remote := range remotes { - if remote.Poller.Period != 0 && counter%remote.Poller.Period == 0 { - toFetch = append(toFetch, remote.Name) - } - } - if len(toFetch) > 0 { - m.Fetch(toFetch) - } - time.Sleep(time.Second) - counter += 1 - } -} diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 5549a1c..9529fd4 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -7,6 +7,7 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" + "github.com/nlewo/comin/internal/prometheus" "github.com/nlewo/comin/internal/types" "github.com/sirupsen/logrus" ) @@ -15,6 +16,7 @@ type repository struct { Repository *git.Repository GitConfig types.GitConfig RepositoryStatus RepositoryStatus + prometheus prometheus.Prometheus } type Repository interface { @@ -22,8 +24,10 @@ type Repository interface { } // repositoryStatus is the last saved repositoryStatus -func New(config types.GitConfig, mainCommitId string) (r *repository, err error) { - r = &repository{} +func New(config types.GitConfig, mainCommitId string, prometheus prometheus.Prometheus) (r *repository, err error) { + r = &repository{ + prometheus: prometheus, + } r.GitConfig = config r.Repository, err = repositoryOpen(config) if err != nil { @@ -50,22 +54,24 @@ func (r *repository) FetchAndUpdate(ctx context.Context, remoteNames []string) ( func (r *repository) Fetch(remoteNames []string) { var err error + var status string r.RepositoryStatus.Error = nil r.RepositoryStatus.ErrorMsg = "" for _, remote := range r.GitConfig.Remotes { repositoryStatusRemote := r.RepositoryStatus.GetRemote(remote.Name) - repositoryStatusRemote.LastFetched = false if !slices.Contains(remoteNames, remote.Name) { continue } - repositoryStatusRemote.LastFetched = true if err = fetch(*r, remote); err != nil { repositoryStatusRemote.FetchErrorMsg = err.Error() + status = "failed" } else { repositoryStatusRemote.FetchErrorMsg = "" repositoryStatusRemote.Fetched = true + status = "succeeded" } repositoryStatusRemote.FetchedAt = time.Now().UTC() + r.prometheus.IncFetchCounter(remote.Name, status) } } diff --git a/internal/repository/repository_status.go b/internal/repository/repository_status.go index 8b88eb3..a323d78 100644 --- a/internal/repository/repository_status.go +++ b/internal/repository/repository_status.go @@ -1,11 +1,10 @@ package repository import ( - "encoding/json" - "fmt" + "time" + deepcopy "github.com/barkimedes/go-deepcopy" "github.com/nlewo/comin/internal/types" - "time" ) type MainBranch struct { @@ -32,9 +31,6 @@ type Remote struct { Testing *TestingBranch `json:"testing,omitempty"` FetchedAt time.Time `json:"fetched_at,omitempty"` Fetched bool `json:"fetched,omitempty"` - // Is this remote the last festched one? This is mainly useful - // to increase Prometheus counters.b - LastFetched bool `json:"last_fetched,omitempty"` } type RepositoryStatus struct { @@ -87,11 +83,6 @@ func (r RepositoryStatus) remoteExists(remoteName string) bool { return false } -func (r RepositoryStatus) Show() { - res, _ := json.MarshalIndent(r, "", "\t") - fmt.Printf("\n%s\n", string(res)) -} - func (r RepositoryStatus) GetRemote(remoteName string) *Remote { for _, remote := range r.Remotes { if remote.Name == remoteName { @@ -102,7 +93,6 @@ func (r RepositoryStatus) GetRemote(remoteName string) *Remote { } func (r RepositoryStatus) Copy() RepositoryStatus { - fmt.Printf("%v", r) rs, err := deepcopy.Anything(r) if err != nil { return RepositoryStatus{} diff --git a/internal/repository/repository_test.go b/internal/repository/repository_test.go index 15c10da..0b4dc63 100644 --- a/internal/repository/repository_test.go +++ b/internal/repository/repository_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/go-git/go-git/v5/plumbing" + "github.com/nlewo/comin/internal/prometheus" "github.com/nlewo/comin/internal/types" "github.com/stretchr/testify/assert" ) @@ -31,7 +32,7 @@ func TestNew(t *testing.T) { }, }, } - r, err := New(gitConfig, "") + r, err := New(gitConfig, "", prometheus.New()) assert.Nil(t, err) assert.Equal(t, "r1", r.RepositoryStatus.Remotes[0].Name) } @@ -60,7 +61,7 @@ func TestPreferMain(t *testing.T) { }, }, } - r, err := New(gitConfig, "") + r, err := New(gitConfig, "", prometheus.New()) assert.Nil(t, err) // r1/main: c1 - c2 - *c3 // r1/testing: c1 - c2 - c3 @@ -115,7 +116,7 @@ func TestMainCommitId(t *testing.T) { }, }, } - r, _ := New(gitConfig, cMain) + r, _ := New(gitConfig, cMain, prometheus.New()) // r1/main: c1 - c2 - c3 - c4 // r1/testing: c1 - c2 - c3 - c4 - c5 @@ -168,7 +169,7 @@ func TestContinueIfHardReset(t *testing.T) { }, }, } - r, _ := New(gitConfig, cMain) + r, _ := New(gitConfig, cMain, prometheus.New()) r.Fetch([]string{"r1", "r2"}) r.Update() @@ -236,7 +237,7 @@ func TestMultipleRemote(t *testing.T) { }, }, } - r, err := New(gitConfig, "") + r, err := New(gitConfig, "", prometheus.New()) assert.Nil(t, err) // r1/main: c1 - c2 - *c3 // r2/main: c1 - c2 - c3 @@ -325,9 +326,7 @@ func TestMultipleRemote(t *testing.T) { assert.Equal(t, "r1", r.RepositoryStatus.SelectedRemoteName) assert.Equal(t, "r1", r.RepositoryStatus.Remotes[0].Name) - assert.False(t, r.RepositoryStatus.Remotes[0].LastFetched) assert.Equal(t, "r2", r.RepositoryStatus.Remotes[1].Name) - assert.True(t, r.RepositoryStatus.Remotes[1].LastFetched) // Fetch the r1 remote // r1/main: c1 - c2 - c3 - c4 - c5 - c6 - c8 - *c9 @@ -378,7 +377,7 @@ func TestTestingSwitch(t *testing.T) { }, }, } - r, _ := New(gitConfig, "") + r, _ := New(gitConfig, "", prometheus.New()) // r1/main: c1 - c2 - *c3 // r1/testing: c1 - c2 - c3 @@ -447,7 +446,7 @@ func TestWithoutTesting(t *testing.T) { }, }, } - r, _ := New(gitConfig, "") + r, _ := New(gitConfig, "", prometheus.New()) r.Fetch([]string{"r1"}) _ = r.Update() @@ -480,7 +479,7 @@ func TestRepositoryUpdateMain(t *testing.T) { }, }, } - r, _ := New(gitConfig, "") + r, _ := New(gitConfig, "", prometheus.New()) // The remote repository is initially checkouted r.Fetch([]string{"origin"}) @@ -539,7 +538,7 @@ func TestRepositoryUpdateHardResetMain(t *testing.T) { }, }, } - r, _ := New(gitConfig, "") + r, _ := New(gitConfig, "", prometheus.New()) // The remote repository is initially checkouted r.Fetch([]string{"origin"}) @@ -598,7 +597,7 @@ func TestRepositoryUpdateTesting(t *testing.T) { }, }, } - r, _ := New(gitConfig, "") + r, _ := New(gitConfig, "", prometheus.New()) // The remote repository is initially checkouted on main r.Fetch([]string{"origin"}) @@ -666,7 +665,7 @@ func TestTestingHardReset(t *testing.T) { }, }, } - r, err := New(gitConfig, "") + r, err := New(gitConfig, "", prometheus.New()) assert.Nil(t, err) // r1/main: c1 - c2 - *c3 // r1/testing: c1 - c2 - c3 diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go new file mode 100644 index 0000000..b6a4fb2 --- /dev/null +++ b/internal/scheduler/scheduler.go @@ -0,0 +1,46 @@ +package scheduler + +import ( + "fmt" + "time" + + "github.com/go-co-op/gocron/v2" + "github.com/nlewo/comin/internal/fetcher" + "github.com/nlewo/comin/internal/types" + "github.com/sirupsen/logrus" +) + +type Scheduler struct { + s gocron.Scheduler +} + +func New() Scheduler { + s, _ := gocron.NewScheduler() + + sched := Scheduler{ + s: s, + } + go sched.s.Start() + return sched +} + +func (s Scheduler) FetchRemotes(fetcher *fetcher.Fetcher, remotes []types.Remote) { + for _, remote := range remotes { + if remote.Poller.Period != 0 { + logrus.Infof("scheduler: starting the period job for the remote '%s' with period %ds", remote.Name, remote.Poller.Period) + s.s.NewJob( + gocron.DurationJob( + time.Duration(remote.Poller.Period)*time.Second, + ), + gocron.NewTask( + func() { + logrus.Debugf("scheduler: running task for remote %s", remote.Name) + fetcher.TriggerFetch([]string{remote.Name}) + }, + ), + gocron.WithSingletonMode(gocron.LimitModeReschedule), + gocron.WithName(fmt.Sprintf("fetch-remote-%s", remote.Name)), + ) + } + } +} diff --git a/internal/store/store.go b/internal/store/store.go index ad4c734..bbb55a3 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -5,14 +5,14 @@ import ( "errors" "os" - "github.com/nlewo/comin/internal/deployment" + "github.com/nlewo/comin/internal/deployer" "github.com/sirupsen/logrus" ) type Data struct { Version string `json:"version"` // Deployments are order from the most recent to older - Deployments []deployment.Deployment `json:"deployments"` + Deployments []deployer.Deployment `json:"deployments"` } type Store struct { @@ -28,13 +28,13 @@ func New(filename string, capacityMain, capacityTesting int) Store { capacityMain: capacityMain, capacityTesting: capacityTesting, } - s.Deployments = make([]deployment.Deployment, 0) + s.Deployments = make([]deployer.Deployment, 0) s.Version = "1" return s } -func (s *Store) DeploymentInsertAndCommit(dpl deployment.Deployment) (ok bool, evicted deployment.Deployment) { +func (s *Store) DeploymentInsertAndCommit(dpl deployer.Deployment) (ok bool, evicted deployer.Deployment) { ok, evicted = s.DeploymentInsert(dpl) if ok { logrus.Infof("The deployment %s has been removed from store.json file", evicted.UUID) @@ -49,7 +49,7 @@ func (s *Store) DeploymentInsertAndCommit(dpl deployment.Deployment) (ok bool, e // DeploymentInsert inserts a deployment and return an evicted // deployment because the capacity has been reached. -func (s *Store) DeploymentInsert(dpl deployment.Deployment) (getsEvicted bool, evicted deployment.Deployment) { +func (s *Store) DeploymentInsert(dpl deployer.Deployment) (getsEvicted bool, evicted deployer.Deployment) { var qty, older int capacity := s.capacityMain if dpl.IsTesting() { @@ -67,15 +67,15 @@ func (s *Store) DeploymentInsert(dpl deployment.Deployment) (getsEvicted bool, e getsEvicted = true s.Deployments = append(s.Deployments[:older], s.Deployments[older+1:]...) } - s.Deployments = append([]deployment.Deployment{dpl}, s.Deployments...) + s.Deployments = append([]deployer.Deployment{dpl}, s.Deployments...) return } -func (s *Store) DeploymentList() []deployment.Deployment { +func (s *Store) DeploymentList() []deployer.Deployment { return s.Deployments } -func (s *Store) LastDeployment() (ok bool, d deployment.Deployment) { +func (s *Store) LastDeployment() (ok bool, d deployer.Deployment) { if len(s.DeploymentList()) > 1 { return true, s.DeploymentList()[0] } diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 9ec5bff..15756d6 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -3,7 +3,7 @@ package store import ( "testing" - "github.com/nlewo/comin/internal/deployment" + "github.com/nlewo/comin/internal/deployer" "github.com/stretchr/testify/assert" ) @@ -19,7 +19,7 @@ func TestDeploymentCommitAndLoad(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 0, len(s.Deployments)) - s.DeploymentInsert(deployment.Deployment{UUID: "1", Operation: "switch"}) + s.DeploymentInsert(deployer.Deployment{UUID: "1", Operation: "switch"}) s.Commit() assert.Nil(t, err) @@ -33,8 +33,8 @@ func TestLastDeployment(t *testing.T) { s := New("", 2, 2) ok, _ := s.LastDeployment() assert.False(t, ok) - s.DeploymentInsert(deployment.Deployment{UUID: "1", Operation: "switch"}) - s.DeploymentInsert(deployment.Deployment{UUID: "2", Operation: "switch"}) + s.DeploymentInsert(deployer.Deployment{UUID: "1", Operation: "switch"}) + s.DeploymentInsert(deployer.Deployment{UUID: "2", Operation: "switch"}) ok, last := s.LastDeployment() assert.True(t, ok) assert.Equal(t, "2", last.UUID) @@ -43,28 +43,28 @@ func TestLastDeployment(t *testing.T) { func TestDeploymentInsert(t *testing.T) { s := New("", 2, 2) var hasEvicted bool - var evicted deployment.Deployment - hasEvicted, _ = s.DeploymentInsert(deployment.Deployment{UUID: "1", Operation: "switch"}) + var evicted deployer.Deployment + hasEvicted, _ = s.DeploymentInsert(deployer.Deployment{UUID: "1", Operation: "switch"}) assert.False(t, hasEvicted) - hasEvicted, _ = s.DeploymentInsert(deployment.Deployment{UUID: "2", Operation: "switch"}) + hasEvicted, _ = s.DeploymentInsert(deployer.Deployment{UUID: "2", Operation: "switch"}) assert.False(t, hasEvicted) - hasEvicted, evicted = s.DeploymentInsert(deployment.Deployment{UUID: "3", Operation: "switch"}) + hasEvicted, evicted = s.DeploymentInsert(deployer.Deployment{UUID: "3", Operation: "switch"}) assert.True(t, hasEvicted) assert.Equal(t, "1", evicted.UUID) - expected := []deployment.Deployment{ + expected := []deployer.Deployment{ {UUID: "3", Operation: "switch"}, {UUID: "2", Operation: "switch"}, } assert.Equal(t, expected, s.DeploymentList()) - hasEvicted, _ = s.DeploymentInsert(deployment.Deployment{UUID: "4", Operation: "test"}) + hasEvicted, _ = s.DeploymentInsert(deployer.Deployment{UUID: "4", Operation: "test"}) assert.False(t, hasEvicted) - hasEvicted, _ = s.DeploymentInsert(deployment.Deployment{UUID: "5", Operation: "test"}) + hasEvicted, _ = s.DeploymentInsert(deployer.Deployment{UUID: "5", Operation: "test"}) assert.False(t, hasEvicted) - hasEvicted, evicted = s.DeploymentInsert(deployment.Deployment{UUID: "6", Operation: "test"}) + hasEvicted, evicted = s.DeploymentInsert(deployer.Deployment{UUID: "6", Operation: "test"}) assert.True(t, hasEvicted) assert.Equal(t, "4", evicted.UUID) - expected = []deployment.Deployment{ + expected = []deployer.Deployment{ {UUID: "6", Operation: "test"}, {UUID: "5", Operation: "test"}, {UUID: "3", Operation: "switch"}, @@ -72,19 +72,19 @@ func TestDeploymentInsert(t *testing.T) { } assert.Equal(t, expected, s.DeploymentList()) - hasEvicted, evicted = s.DeploymentInsert(deployment.Deployment{UUID: "7", Operation: "switch"}) + hasEvicted, evicted = s.DeploymentInsert(deployer.Deployment{UUID: "7", Operation: "switch"}) assert.True(t, hasEvicted) assert.Equal(t, "2", evicted.UUID) - expected = []deployment.Deployment{ + expected = []deployer.Deployment{ {UUID: "6", Operation: "test"}, {UUID: "5", Operation: "test"}, {UUID: "7", Operation: "switch"}, {UUID: "3", Operation: "switch"}, } - hasEvicted, evicted = s.DeploymentInsert(deployment.Deployment{UUID: "8", Operation: "switch"}) + hasEvicted, evicted = s.DeploymentInsert(deployer.Deployment{UUID: "8", Operation: "switch"}) assert.True(t, hasEvicted) assert.Equal(t, "3", evicted.UUID) - expected = []deployment.Deployment{ + expected = []deployer.Deployment{ {UUID: "6", Operation: "test"}, {UUID: "5", Operation: "test"}, {UUID: "8", Operation: "switch"}, diff --git a/internal/utils/testing.go b/internal/utils/testing.go new file mode 100644 index 0000000..d9ade85 --- /dev/null +++ b/internal/utils/testing.go @@ -0,0 +1,26 @@ +package utils + +import ( + "context" + + "github.com/nlewo/comin/internal/repository" +) + +type RepositoryMock struct { + RsCh chan repository.RepositoryStatus +} + +func NewRepositoryMock() (r *RepositoryMock) { + rsCh := make(chan repository.RepositoryStatus, 5) + return &RepositoryMock{ + RsCh: rsCh, + } +} +func (r *RepositoryMock) FetchAndUpdate(ctx context.Context, remoteNames []string) (rsCh chan repository.RepositoryStatus) { + return r.RsCh +} +func (r *RepositoryMock) Fetch(remoteNames []string) { +} +func (r *RepositoryMock) Update() (repository.RepositoryStatus, error) { + return repository.RepositoryStatus{}, nil +}