From adfa042b8a9c97334b7fc7f90d89a3cfc659e23d Mon Sep 17 00:00:00 2001 From: Dmitry Gritsay Date: Thu, 8 Aug 2019 17:39:41 +0300 Subject: [PATCH 1/6] new interfaces has been added --- .../upgradeagent/dockercomposedeployment.go | 36 +++++++++++++++++++ .../app/upgradeagent/kubernetesdeployment.go | 36 +++++++++++++++++++ .../app/upgradeagent/mockupstreamclient.go | 14 ++++++++ internal/app/upgradeagent/objects.go | 13 +++++++ 4 files changed, 99 insertions(+) create mode 100644 internal/app/upgradeagent/dockercomposedeployment.go create mode 100644 internal/app/upgradeagent/kubernetesdeployment.go create mode 100644 internal/app/upgradeagent/mockupstreamclient.go diff --git a/internal/app/upgradeagent/dockercomposedeployment.go b/internal/app/upgradeagent/dockercomposedeployment.go new file mode 100644 index 0000000..8b3290e --- /dev/null +++ b/internal/app/upgradeagent/dockercomposedeployment.go @@ -0,0 +1,36 @@ +package upgradeagent + +type dockerComposeDeployment struct { + upgradeInfo *UpstreamResponseUpgradeInfo + upstreamClient UpstreamClient +} + +// NewDockerComposeDeployment creates a new instance of docker-compose deployment kind. +func NewDockerComposeDeployment(upstreamServiceClient UpstreamClient) DeploymentClient { + return &dockerComposeDeployment{upgradeInfo: NewUpstreamResponseUpgradeInfo(), upstreamClient: upstreamServiceClient} +} + +// UpgradeCheck do check if there is a new version of the current solution available +func (d *dockerComposeDeployment) UpgradeCheck(localSolutionInfo LocalSolutionInfo) bool { + if upgradeInfo, isNewVersion := d.upstreamClient.RequestUpgrade(localSolutionInfo); isNewVersion { + d.upgradeInfo = upgradeInfo + return true + } + return false +} + +// Upgrade does upgrade the current solution +func (d *dockerComposeDeployment) Upgrade() error { + return nil +} + +// Rollback does rollback the current solution to the previous state in case both upgrade or health check is fails +func (d *dockerComposeDeployment) Rollback() { + // TBD +} + +// HealthCheck does a health check of the current solution after the upgrade +func (d *dockerComposeDeployment) HealthCheck() bool { + d.upgradeInfo.HealthCheckStatus = Unhealthy + return false +} diff --git a/internal/app/upgradeagent/kubernetesdeployment.go b/internal/app/upgradeagent/kubernetesdeployment.go new file mode 100644 index 0000000..dfa61ae --- /dev/null +++ b/internal/app/upgradeagent/kubernetesdeployment.go @@ -0,0 +1,36 @@ +package upgradeagent + +type kubernetesDeployment struct { + upgradeInfo *UpstreamResponseUpgradeInfo + upstreamClient UpstreamClient +} + +// NewKubernetesDeployment creates a new instance of kubernetes deployment kind. +func NewKubernetesDeployment(upstreamServiceClient UpstreamClient) DeploymentClient { + return &kubernetesDeployment{upgradeInfo: NewUpstreamResponseUpgradeInfo(), upstreamClient: upstreamServiceClient} +} + +// UpgradeCheck do check if there is a new version of the current solution available +func (d *kubernetesDeployment) UpgradeCheck(localSolutionInfo LocalSolutionInfo) bool { + if upgradeInfo, isNewVersion := d.upstreamClient.RequestUpgrade(localSolutionInfo); isNewVersion { + d.upgradeInfo = upgradeInfo + return true + } + return false +} + +// Upgrade does upgrade the current solution +func (d *kubernetesDeployment) Upgrade() error { + return nil +} + +// Rollback does rollback the current solution to the previous state in case both upgrade or health check is fails +func (d *kubernetesDeployment) Rollback() { + // TBD +} + +// HealthCheck does a health check of the current solution after the upgrade +func (d *kubernetesDeployment) HealthCheck() bool { + d.upgradeInfo.HealthCheckStatus = Unhealthy + return false +} diff --git a/internal/app/upgradeagent/mockupstreamclient.go b/internal/app/upgradeagent/mockupstreamclient.go new file mode 100644 index 0000000..04cffd9 --- /dev/null +++ b/internal/app/upgradeagent/mockupstreamclient.go @@ -0,0 +1,14 @@ +package upgradeagent + +type mockUpstreamClient struct { +} + +// NewMockUpstreamClient returns a new instance of mock upstream service client +func NewMockUpstreamClient() UpstreamClient { + return mockUpstreamClient{} +} + +// RequestUpgrade implements the transport layer between Patrao and mocked upstream service +func (mock mockUpstreamClient) RequestUpgrade(solutionInfo LocalSolutionInfo) (*UpstreamResponseUpgradeInfo, bool) { + return nil, false +} diff --git a/internal/app/upgradeagent/objects.go b/internal/app/upgradeagent/objects.go index 66ee7e9..a35dfa1 100644 --- a/internal/app/upgradeagent/objects.go +++ b/internal/app/upgradeagent/objects.go @@ -4,6 +4,19 @@ import ( "time" ) +// DeploymentClient is a common interface for any deployment kinds. +type DeploymentClient interface { + UpgradeCheck(LocalSolutionInfo) bool + Upgrade() error + HealthCheck() bool + Rollback() +} + +// UpstreamClient is a common interface for any upstream service kinds +type UpstreamClient interface { + RequestUpgrade(LocalSolutionInfo) (*UpstreamResponseUpgradeInfo, bool) +} + // KindType Kind type for all structures type KindType string From 717f8522be013187ecd40b7605d44c4ccc666480 Mon Sep 17 00:00:00 2001 From: Dmitry Gritsay Date: Mon, 12 Aug 2019 16:29:01 +0300 Subject: [PATCH 2/6] refactoring - move existing functionality under DeploymentClinet implementation --- cmd/upgradeagent/main.go | 6 + internal/app/upgradeagent/constants.go | 12 + .../upgradeagent/docker_compose_deployment.go | 252 ++++++++++++++++++ .../upgradeagent/dockercomposedeployment.go | 36 --- internal/app/upgradeagent/dockermanager.go | 53 +--- .../app/upgradeagent/dockermanager_test.go | 25 +- .../app/upgradeagent/kubernetes_deployment.go | 55 ++++ .../app/upgradeagent/kubernetesdeployment.go | 36 --- .../app/upgradeagent/mock_upstream_client.go | 56 ++++ .../app/upgradeagent/mockupstreamclient.go | 14 - internal/app/upgradeagent/objects.go | 12 +- internal/app/upgradeagent/upgradeagent.go | 249 ++--------------- internal/app/upgradeagent/utils.go | 41 ++- internal/app/upgradeagent/utils_test.go | 38 +-- 14 files changed, 467 insertions(+), 418 deletions(-) create mode 100644 internal/app/upgradeagent/docker_compose_deployment.go delete mode 100644 internal/app/upgradeagent/dockercomposedeployment.go create mode 100644 internal/app/upgradeagent/kubernetes_deployment.go delete mode 100644 internal/app/upgradeagent/kubernetesdeployment.go create mode 100644 internal/app/upgradeagent/mock_upstream_client.go delete mode 100644 internal/app/upgradeagent/mockupstreamclient.go diff --git a/cmd/upgradeagent/main.go b/cmd/upgradeagent/main.go index ff525a4..d8a3192 100644 --- a/cmd/upgradeagent/main.go +++ b/cmd/upgradeagent/main.go @@ -33,6 +33,12 @@ func setupAppFlags() []cli.Flag { Value: core.UpgradeIntervalValue, EnvVar: core.UpgradeIntervalValueEnvVar, }, + cli.StringFlag{ + Name: core.UpstreamTypeName, + Usage: core.UpstreamTypeUsage, + Value: core.UpstreamTypeValue, + EnvVar: core.UpstreamTypeValueEnvVar, + }, cli.BoolFlag{ Name: core.RunOnceName, Usage: core.RunOnceUsage, diff --git a/internal/app/upgradeagent/constants.go b/internal/app/upgradeagent/constants.go index 5b098be..cb1b932 100644 --- a/internal/app/upgradeagent/constants.go +++ b/internal/app/upgradeagent/constants.go @@ -42,6 +42,18 @@ const UpstreamValue = "http://localhost:1080" // UpstreamEnvVar is env variable for UpstreamName const UpstreamEnvVar = "PATRAO_UPSTREAM_HOST" +// UpstreamTypeName is a command line parameter for define which upstream kind should be used +const UpstreamTypeName = "upstreamType" + +// UpstreamTypeValue is default value for UpstreamType +const UpstreamTypeValue = "mock" + +// UpstreamTypeUsage is description of UpstreamType parameter +const UpstreamTypeUsage = "define which upstream type should be used by Patrao Agent (possible values: --upstreamType=mock)" + +// UpstreamTypeValueEnvVar is env variable for UpstreamType parameter +const UpstreamTypeValueEnvVar = "PATRAO_UPSTREAM_TYPE" + // DockerComposeFileName is defaut file name for *.yml scripts const DockerComposeFileName = "docker-compose.yml" diff --git a/internal/app/upgradeagent/docker_compose_deployment.go b/internal/app/upgradeagent/docker_compose_deployment.go new file mode 100644 index 0000000..2e2cc2d --- /dev/null +++ b/internal/app/upgradeagent/docker_compose_deployment.go @@ -0,0 +1,252 @@ +package upgradeagent + +import ( + "os" + "os/exec" + "path" + "path/filepath" + "time" + + "github.com/docker/docker/api/types" + log "github.com/sirupsen/logrus" + "github.com/urfave/cli" + "gopkg.in/yaml.v2" +) + +type dockerComposeDeployment struct { + upgradeInfo *UpstreamResponseUpgradeInfo + upstreamClient UpstreamClient + context *cli.Context + dockerClient DockerClient + localSolutionInfo *LocalSolutionInfo +} + +// NewDockerComposeDeployment creates a new instance of docker-compose deployment kind. +func NewDockerComposeDeployment(ctx *cli.Context, upstreamServiceClient UpstreamClient, dockerCli DockerClient, solutionInfo *LocalSolutionInfo) DeploymentClient { + return &dockerComposeDeployment{ + upgradeInfo: NewUpstreamResponseUpgradeInfo(), + upstreamClient: upstreamServiceClient, + context: ctx, + dockerClient: dockerCli, + localSolutionInfo: solutionInfo, + } +} + +// CheckUpgrade do check if there is a new version of the current solution available +func (d *dockerComposeDeployment) CheckUpgrade() bool { + upgradeInfo, err := d.upstreamClient.RequestUpgrade(*d.localSolutionInfo) + if err != nil { + log.Error(err) + return false + } + containers, err := d.dockerClient.ListContainers() + if err != nil { + log.Error(err) + return false + } + if isNewVersion(upgradeInfo, containers) == false { + log.Infof("Solution [%s] is up-to-date.", upgradeInfo.Name) + return false + } + d.upgradeInfo = upgradeInfo + return true +} + +// Upgrade does upgrade the current solution +func (d *dockerComposeDeployment) DoUpgrade() error { + containers, err := d.dockerClient.ListContainers() + if err != nil { + log.Error(err) + return err + } + for _, container := range containers { + name, found := container.GetProjectName() + if !found { + continue + } + if d.localSolutionInfo.name == name { + err := d.dockerClient.StopContainer(container, DefaultTimeoutS*time.Second) + if err != nil { + log.Error(err) + } + } + } + err = d.LaunchSolution() + if err != nil { + log.Error(err) + return err + } + log.Infof("Solution [%s] is successful launched", d.localSolutionInfo.name) + + return nil +} + +// CheckHealth does a health check of the current solution after the upgrade +func (d *dockerComposeDeployment) CheckHealth() bool { + timeout := time.After(time.Duration(d.upgradeInfo.ThresholdTimeS) * time.Second) + for d.upgradeInfo.HealthCheckStatus == Undefined { + select { + case <-timeout: + d.upgradeInfo.HealthCheckStatus = Unhealthy + log.Infof("Solution [%s] is Unhealthy", d.upgradeInfo.Name) + return false + default: + { + checkContainersCompletedCount := 0 + + for _, healthChekCmd := range d.upgradeInfo.HealthCheckCmds { + container, err := d.dockerClient.GetContainerByName(d.upgradeInfo.Name, healthChekCmd.ContainerName) + if err != nil { + log.Error(err) + break + } + config, err := d.dockerClient.InspectContainer(container) + if err != nil { + log.Error(err) + break + } + if !config.State.Running { + log.Infof("Container %s is NOT Running state.", healthChekCmd.ContainerName) + continue + } + if config.State.Health != nil { + log.Infof("Container %s has embedded healthchek.", healthChekCmd.ContainerName) + if config.State.Health.Status != types.Healthy { + log.Infof("Container %s have healthy information. The current status is [%s]", healthChekCmd.ContainerName, config.State.Health.Status) + continue + } + } else { + log.Infof("Container %s has NOT embedded healthchek. Skip this step.", healthChekCmd.ContainerName) + } + exitCode, err := d.dockerClient.ExecContainer(container, healthChekCmd.Cmd) + if err != nil { + log.Error(err) + break + } + if exitCode == 0 { + log.Infof("Container %s has passed healthcheck command [%s], exit code is [%d]", healthChekCmd.ContainerName, healthChekCmd.Cmd, exitCode) + checkContainersCompletedCount++ + } + } + if checkContainersCompletedCount == len(d.upgradeInfo.HealthCheckCmds) { + log.Infof("Solution [%s] is healthy", d.upgradeInfo.Name) + d.upgradeInfo.HealthCheckStatus = Healthy + return true + } + time.Sleep(1 * time.Second) + } + } + } + return false +} + +// GetLocalSolutionInfo returns pointer to LocalSolutionInfo data structure +func (d *dockerComposeDeployment) GetLocalSolutionInfo() *LocalSolutionInfo { + return d.localSolutionInfo +} + +// isNewVersion check if there are new version available. +func isNewVersion(upgradeInfo *UpstreamResponseUpgradeInfo, containers []Container) bool { + var rc bool + + rc = false + containersSpec := make(map[string]ContainerSpec) + specMap := make(map[string]interface{}) + err := yaml.Unmarshal([]byte(upgradeInfo.Spec), specMap) + if nil != err { + log.Error(err) + return false + } + if val, exists := specMap[DockerComposeServicesName]; exists { + servicesMap := val.(map[interface{}]interface{}) + for service := range servicesMap { + var ( + serviceImageName string + found bool + ok bool + ) + details := servicesMap[service].(map[interface{}]interface{}) + found = false + for item := range details { + if DockerComposeImageName == item { + val, itemFound := details[item] + if itemFound { + serviceImageName, ok = val.(string) + if ok { + found = true + break + } + } + } + } + if found { + containersSpec[service.(string)] = ContainerSpec{Name: service.(string), Image: serviceImageName} + } + } + for _, container := range containers { + solutionName, found := container.GetProjectName() + if !found { + continue + } + serviceName, found := container.GetServiceName() + if !found { + log.Error(err) + continue + } + val, exist := containersSpec[serviceName] + if (exist) && (upgradeInfo.Name == solutionName) { + if container.ImageName() != val.Image { + rc = true + break + } + } + } + } + return rc +} + +// LaunchSolution launch solution based on the received docker-compose specification +func (d *dockerComposeDeployment) LaunchSolution() error { + if _, isFileExist := os.Stat(d.upgradeInfo.Name); !os.IsNotExist(isFileExist) { + os.RemoveAll(d.upgradeInfo.Name) + } + err := os.Mkdir(d.upgradeInfo.Name, os.ModePerm) + if nil != err { + log.Error(err) + return err + } + defer os.Remove(d.upgradeInfo.Name) + dockerComposeFileName := path.Join(d.upgradeInfo.Name, DockerComposeFileName) + f, err := os.Create(dockerComposeFileName) + if err != nil { + log.Error(err) + return err + } + + defer func() { + f.Close() + os.Remove(dockerComposeFileName) + }() + + _, err = f.Write([]byte(d.upgradeInfo.Spec)) + if err != nil { + log.Error(err) + return err + } + log.Infof("Launching solution [%s]", d.upgradeInfo.Name) + ex, _ := os.Executable() + rootPath := filepath.Dir(ex) + cmd := exec.Command(DockerComposeCommand, "-f", path.Join(rootPath, dockerComposeFileName), "up", "-d") + err = cmd.Run() + if err != nil { + log.Error(err) + return err + } + + return nil +} + +// DoRollback does rollback the current solution to the previous state in case both upgrade or health check is fails +func (d *dockerComposeDeployment) DoRollback() { + // TBD +} diff --git a/internal/app/upgradeagent/dockercomposedeployment.go b/internal/app/upgradeagent/dockercomposedeployment.go deleted file mode 100644 index 8b3290e..0000000 --- a/internal/app/upgradeagent/dockercomposedeployment.go +++ /dev/null @@ -1,36 +0,0 @@ -package upgradeagent - -type dockerComposeDeployment struct { - upgradeInfo *UpstreamResponseUpgradeInfo - upstreamClient UpstreamClient -} - -// NewDockerComposeDeployment creates a new instance of docker-compose deployment kind. -func NewDockerComposeDeployment(upstreamServiceClient UpstreamClient) DeploymentClient { - return &dockerComposeDeployment{upgradeInfo: NewUpstreamResponseUpgradeInfo(), upstreamClient: upstreamServiceClient} -} - -// UpgradeCheck do check if there is a new version of the current solution available -func (d *dockerComposeDeployment) UpgradeCheck(localSolutionInfo LocalSolutionInfo) bool { - if upgradeInfo, isNewVersion := d.upstreamClient.RequestUpgrade(localSolutionInfo); isNewVersion { - d.upgradeInfo = upgradeInfo - return true - } - return false -} - -// Upgrade does upgrade the current solution -func (d *dockerComposeDeployment) Upgrade() error { - return nil -} - -// Rollback does rollback the current solution to the previous state in case both upgrade or health check is fails -func (d *dockerComposeDeployment) Rollback() { - // TBD -} - -// HealthCheck does a health check of the current solution after the upgrade -func (d *dockerComposeDeployment) HealthCheck() bool { - d.upgradeInfo.HealthCheckStatus = Unhealthy - return false -} diff --git a/internal/app/upgradeagent/dockermanager.go b/internal/app/upgradeagent/dockermanager.go index f51e1b3..6084d02 100644 --- a/internal/app/upgradeagent/dockermanager.go +++ b/internal/app/upgradeagent/dockermanager.go @@ -3,10 +3,6 @@ package upgradeagent import ( "errors" "fmt" - "os" - "os/exec" - "path" - "path/filepath" "strings" "time" @@ -20,15 +16,13 @@ import ( type DockerClient interface { ListContainers() ([]Container, error) StopContainer(Container, time.Duration) error - LaunchSolution(*UpstreamResponseUpgradeInfo) error InspectContainer(*Container) (types.ContainerJSON, error) ExecContainer(*Container, string) (int, error) GetContainerByName(string, string) (*Container, error) } type dockerClient struct { - api dockerclient.CommonAPIClient - pullImages bool + api dockerclient.CommonAPIClient } // NewClient returns a new Client instance which can be used to interact with @@ -37,12 +31,12 @@ type dockerClient struct { // * DOCKER_HOST the docker-engine host to send api requests to // * DOCKER_TLS_VERIFY whether to verify tls certificates // * DOCKER_API_VERSION the minimum docker api version to work with -func NewClient(pullImages bool) DockerClient { +func NewClient() DockerClient { client, err := dockerclient.NewEnvClient() if err != nil { log.Fatalf("Error instantiating Docker client: %s", err) } - return dockerClient{api: client, pullImages: pullImages} + return dockerClient{api: client} } func (client dockerClient) ListContainers() ([]Container, error) { @@ -117,47 +111,6 @@ func (client dockerClient) waitForStop(c Container, waitTime time.Duration) erro } } -// LaunchSolution launch solution based on the received docker-compose specification -func (client dockerClient) LaunchSolution(info *UpstreamResponseUpgradeInfo) error { - if _, isFileExist := os.Stat(info.Name); !os.IsNotExist(isFileExist) { - os.RemoveAll(info.Name) - } - err := os.Mkdir(info.Name, os.ModePerm) - if nil != err { - log.Error(err) - return err - } - defer os.Remove(info.Name) - dockerComposeFileName := path.Join(info.Name, DockerComposeFileName) - f, err := os.Create(dockerComposeFileName) - if err != nil { - log.Error(err) - return err - } - - defer func() { - f.Close() - os.Remove(dockerComposeFileName) - }() - - _, err = f.Write([]byte(info.Spec)) - if err != nil { - log.Error(err) - return err - } - log.Infof("Launching solution [%s]", info.Name) - ex, _ := os.Executable() - rootPath := filepath.Dir(ex) - cmd := exec.Command(DockerComposeCommand, "-f", path.Join(rootPath, dockerComposeFileName), "up", "-d") - err = cmd.Run() - if err != nil { - log.Error(err) - return err - } - - return nil -} - //InspectContainer returns container configuration data structure func (client dockerClient) InspectContainer(c *Container) (types.ContainerJSON, error) { return client.api.ContainerInspect(context.Background(), c.ID()) diff --git a/internal/app/upgradeagent/dockermanager_test.go b/internal/app/upgradeagent/dockermanager_test.go index 1ed2b2c..104fc90 100644 --- a/internal/app/upgradeagent/dockermanager_test.go +++ b/internal/app/upgradeagent/dockermanager_test.go @@ -20,14 +20,12 @@ func CreateUpstreamResponseUpgradeInfo(t *testing.T) *core.UpstreamResponseUpgra } func TestNewClient(t *testing.T) { - client := core.NewClient(false) - assert.NotNil(t, client) - client = core.NewClient(true) + client := core.NewClient() assert.NotNil(t, client) } func TestGetContainerByName(t *testing.T) { - client := core.NewClient(false) + client := core.NewClient() assert.NotNil(t, client) c, err := client.GetContainerByName("test", "db") assert.Nil(t, c) @@ -35,7 +33,7 @@ func TestGetContainerByName(t *testing.T) { } func TestListContainers(t *testing.T) { - client := core.NewClient(false) + client := core.NewClient() assert.NotNil(t, client) list, err := client.ListContainers() assert.Nil(t, err) @@ -43,7 +41,7 @@ func TestListContainers(t *testing.T) { } func TestStopContainer(t *testing.T) { - client := core.NewClient(false) + client := core.NewClient() assert.NotNil(t, client) c := CreateTestContainer(t, containerInfo, imageInfo) @@ -52,7 +50,7 @@ func TestStopContainer(t *testing.T) { } func TestInspectContainer(t *testing.T) { - client := core.NewClient(false) + client := core.NewClient() assert.NotNil(t, client) c := CreateTestContainer(t, containerInfo, imageInfo) @@ -61,21 +59,10 @@ func TestInspectContainer(t *testing.T) { } func TestExecContainer(t *testing.T) { - client := core.NewClient(false) + client := core.NewClient() assert.NotNil(t, client) c := CreateTestContainer(t, containerInfo, imageInfo) _, err := client.ExecContainer(c, "/bin/bash") assert.Error(t, err) } - -func TestLaunchSolution(t *testing.T) { - client := core.NewClient(false) - assert.NotNil(t, client) - - info := CreateUpstreamResponseUpgradeInfo(t) - assert.NotNil(t, info) - - err := client.LaunchSolution(info) - assert.Error(t, err) -} diff --git a/internal/app/upgradeagent/kubernetes_deployment.go b/internal/app/upgradeagent/kubernetes_deployment.go new file mode 100644 index 0000000..16e0eac --- /dev/null +++ b/internal/app/upgradeagent/kubernetes_deployment.go @@ -0,0 +1,55 @@ +package upgradeagent + +import ( + "github.com/urfave/cli" +) + +type kubernetesDeployment struct { + upgradeInfo *UpstreamResponseUpgradeInfo + upstreamClient UpstreamClient + context *cli.Context + dockerClient DockerClient + localSolutionInfo *LocalSolutionInfo +} + +// NewKubernetesDeployment creates a new instance of kubernetes deployment kind. +func NewKubernetesDeployment(ctx *cli.Context, upstreamServiceClient UpstreamClient, dockerCli DockerClient, solutionInfo *LocalSolutionInfo) DeploymentClient { + return &kubernetesDeployment{ + upgradeInfo: NewUpstreamResponseUpgradeInfo(), + upstreamClient: upstreamServiceClient, + context: ctx, + dockerClient: dockerCli, + localSolutionInfo: solutionInfo, + } +} + +// GetLocalSolutionInfo returns pointer to LocalSolutionInfo data structure +func (d *kubernetesDeployment) GetLocalSolutionInfo() *LocalSolutionInfo { + return d.localSolutionInfo +} + +// CheckUpgrade do check if there is a new version of the current solution available +func (d *kubernetesDeployment) CheckUpgrade() bool { + return false +} + +// DoUpgrade does upgrade the current solution +func (d *kubernetesDeployment) DoUpgrade() error { + return nil +} + +// DoRollback does rollback the current solution to the previous state in case both upgrade or health check is fails +func (d *kubernetesDeployment) DoRollback() { + // TBD +} + +// CheckHealth does a health check of the current solution after the upgrade +func (d *kubernetesDeployment) CheckHealth() bool { + d.upgradeInfo.HealthCheckStatus = Unhealthy + return false +} + +// LaunchSolution starts solution using UpstreamResponseUpgradeInfo data structure +func (d *kubernetesDeployment) LaunchSolution() error { + return nil +} diff --git a/internal/app/upgradeagent/kubernetesdeployment.go b/internal/app/upgradeagent/kubernetesdeployment.go deleted file mode 100644 index dfa61ae..0000000 --- a/internal/app/upgradeagent/kubernetesdeployment.go +++ /dev/null @@ -1,36 +0,0 @@ -package upgradeagent - -type kubernetesDeployment struct { - upgradeInfo *UpstreamResponseUpgradeInfo - upstreamClient UpstreamClient -} - -// NewKubernetesDeployment creates a new instance of kubernetes deployment kind. -func NewKubernetesDeployment(upstreamServiceClient UpstreamClient) DeploymentClient { - return &kubernetesDeployment{upgradeInfo: NewUpstreamResponseUpgradeInfo(), upstreamClient: upstreamServiceClient} -} - -// UpgradeCheck do check if there is a new version of the current solution available -func (d *kubernetesDeployment) UpgradeCheck(localSolutionInfo LocalSolutionInfo) bool { - if upgradeInfo, isNewVersion := d.upstreamClient.RequestUpgrade(localSolutionInfo); isNewVersion { - d.upgradeInfo = upgradeInfo - return true - } - return false -} - -// Upgrade does upgrade the current solution -func (d *kubernetesDeployment) Upgrade() error { - return nil -} - -// Rollback does rollback the current solution to the previous state in case both upgrade or health check is fails -func (d *kubernetesDeployment) Rollback() { - // TBD -} - -// HealthCheck does a health check of the current solution after the upgrade -func (d *kubernetesDeployment) HealthCheck() bool { - d.upgradeInfo.HealthCheckStatus = Unhealthy - return false -} diff --git a/internal/app/upgradeagent/mock_upstream_client.go b/internal/app/upgradeagent/mock_upstream_client.go new file mode 100644 index 0000000..7a2ce7a --- /dev/null +++ b/internal/app/upgradeagent/mock_upstream_client.go @@ -0,0 +1,56 @@ +package upgradeagent + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + log "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +type mockUpstreamClient struct { + context *cli.Context +} + +// NewMockUpstreamClient returns a new instance of mock upstream service client +func NewMockUpstreamClient(ctx *cli.Context) UpstreamClient { + return &mockUpstreamClient{context: ctx} +} + +// RequestUpgrade implements the transport layer between Patrao and mocked upstream service +func (mock *mockUpstreamClient) RequestUpgrade(solutionInfo LocalSolutionInfo) (*UpstreamResponseUpgradeInfo, error) { + var currentPath string + + mockURL := mock.context.GlobalString(UpstreamName) + UpstreamGetUpgrade + upgradeInfo := NewUpstreamResponseUpgradeInfo() + + if mockURL[len(mockURL)-1:] != "/" { + currentPath = mockURL + "/" + solutionInfo.GetName() + } else { + currentPath = mockURL + solutionInfo.GetName() + } + + resp, err := http.Get(currentPath) + if nil != err { + log.Error(err) + return NewUpstreamResponseUpgradeInfo(), err + } + + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if nil != err { + log.Error(err) + return NewUpstreamResponseUpgradeInfo(), err + } + + err = json.Unmarshal([]byte(body), &upgradeInfo) + + if nil != err { + log.Error(err) + return NewUpstreamResponseUpgradeInfo(), err + } + + return upgradeInfo, nil +} diff --git a/internal/app/upgradeagent/mockupstreamclient.go b/internal/app/upgradeagent/mockupstreamclient.go deleted file mode 100644 index 04cffd9..0000000 --- a/internal/app/upgradeagent/mockupstreamclient.go +++ /dev/null @@ -1,14 +0,0 @@ -package upgradeagent - -type mockUpstreamClient struct { -} - -// NewMockUpstreamClient returns a new instance of mock upstream service client -func NewMockUpstreamClient() UpstreamClient { - return mockUpstreamClient{} -} - -// RequestUpgrade implements the transport layer between Patrao and mocked upstream service -func (mock mockUpstreamClient) RequestUpgrade(solutionInfo LocalSolutionInfo) (*UpstreamResponseUpgradeInfo, bool) { - return nil, false -} diff --git a/internal/app/upgradeagent/objects.go b/internal/app/upgradeagent/objects.go index a35dfa1..33a25f0 100644 --- a/internal/app/upgradeagent/objects.go +++ b/internal/app/upgradeagent/objects.go @@ -6,15 +6,17 @@ import ( // DeploymentClient is a common interface for any deployment kinds. type DeploymentClient interface { - UpgradeCheck(LocalSolutionInfo) bool - Upgrade() error - HealthCheck() bool - Rollback() + CheckUpgrade() bool + DoUpgrade() error + CheckHealth() bool + DoRollback() + LaunchSolution() error + GetLocalSolutionInfo() *LocalSolutionInfo } // UpstreamClient is a common interface for any upstream service kinds type UpstreamClient interface { - RequestUpgrade(LocalSolutionInfo) (*UpstreamResponseUpgradeInfo, bool) + RequestUpgrade(LocalSolutionInfo) (*UpstreamResponseUpgradeInfo, error) } // KindType Kind type for all structures diff --git a/internal/app/upgradeagent/upgradeagent.go b/internal/app/upgradeagent/upgradeagent.go index 4cf6a70..1c306aa 100644 --- a/internal/app/upgradeagent/upgradeagent.go +++ b/internal/app/upgradeagent/upgradeagent.go @@ -1,56 +1,41 @@ package upgradeagent import ( - "encoding/json" - "io/ioutil" - "net/http" - "time" - - "github.com/docker/docker/api/types" "github.com/jasonlvhit/gocron" log "github.com/sirupsen/logrus" "github.com/urfave/cli" - "gopkg.in/yaml.v2" -) - -var ( - client DockerClient ) // Main - Upgrade Agent entry point func Main(context *cli.Context) error { - client = NewClient(false) - if context.GlobalBool(RunOnceName) { return runOnce(context) } return schedulePeriodicUpgrades(context) } +// runOnce do check launched solutions and do upgrade them if there are new versions available func runOnce(context *cli.Context) error { log.Infoln("[+]runOnce()") - containers, rc := client.ListContainers() - if nil != rc { - log.Error(rc) - log.Infoln("[-]runOnce()") - return rc - } - if len(containers) == 0 { - log.Info("There are no launched containers on the host") - log.Infoln("[-]runOnce()") - return nil - } - upgradeInfoArray, rc := getLaunchedSolutionsList(context, &containers) - if nil != rc { - log.Error(rc) - log.Infoln("[-]runOnce()") - return rc + localSolutionList := GetLocalSolutionList(context, NewClient()) + for _, current := range localSolutionList { + if current.CheckUpgrade() == true { + err := current.DoUpgrade() + if err != nil { + log.Error(err) + current.DoRollback() + continue + } + if current.CheckHealth() == false { + current.DoRollback() + } + } } - rc = doUpgradeSolutions(upgradeInfoArray, &containers) log.Infoln("[-]runOnce()") - return rc + return nil } +// schedulePeriodicUpgrades schedules pereodic upgrade check using upgrade interval command line parameter func schedulePeriodicUpgrades(context *cli.Context) error { log.Infoln("[+]schedulePeriodicUpgrades()") { @@ -60,205 +45,3 @@ func schedulePeriodicUpgrades(context *cli.Context) error { log.Infoln("[-]schedulePeriodicUpgrades()") return nil } - -func getLaunchedSolutionsList(context *cli.Context, containers *[]Container) (map[string]*UpstreamResponseUpgradeInfo, error) { - var ( - err error - currentPath string - ) - getURLPath := context.GlobalString(UpstreamName) + UpstreamGetUpgrade - rc := make(map[string]*UpstreamResponseUpgradeInfo) - runningSolutions := GetLocalSolutionList(*containers) - for _, current := range runningSolutions { - if getURLPath[len(getURLPath)-1:] != "/" { - currentPath = getURLPath + "/" + current.GetName() - } else { - currentPath = getURLPath + current.GetName() - - } - resp, err := http.Get(currentPath) - if nil != err { - log.Error(err) - continue - } - - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if nil != err { - log.Error(err) - continue - } - - toUpgrade := NewUpstreamResponseUpgradeInfo() - err = json.Unmarshal([]byte(body), &toUpgrade) - - if nil != err { - log.Error(err) - continue - } - - rc[toUpgrade.Name] = toUpgrade - } - return rc, err -} - -func doUpgradeSolutions(upgradeInfoList map[string]*UpstreamResponseUpgradeInfo, containers *[]Container) error { - var ( - rc error - ) - toCheck := make(map[string]*UpstreamResponseUpgradeInfo) - for _, item := range upgradeInfoList { - if !isNewVersion(item, containers) { - log.Infof("Solution [%s] is up-to-date.", item.Name) - continue - } - for _, container := range *containers { - name, found := container.GetProjectName() - if !found { - continue - } - if item.Name == name { - err := client.StopContainer(container, DefaultTimeoutS*time.Second) - if err != nil { - log.Error(err) - } - } - } - err := client.LaunchSolution(item) - if err != nil { - log.Error(err) - continue - } - log.Infof("Solution [%s] is successful launched", item.Name) - toCheck[item.Name] = item - } - doHealthChek(toCheck) - - return rc -} - -// isNewVersion check if there are new version available. -func isNewVersion(upgradeInfo *UpstreamResponseUpgradeInfo, containers *[]Container) bool { - var rc bool - - rc = false - containersSpec := make(map[string]ContainerSpec) - specMap := make(map[string]interface{}) - err := yaml.Unmarshal([]byte(upgradeInfo.Spec), specMap) - if nil != err { - log.Error(err) - return false - } - if val, exists := specMap[DockerComposeServicesName]; exists { - servicesMap := val.(map[interface{}]interface{}) - for service := range servicesMap { - var ( - serviceImageName string - found bool - ok bool - ) - details := servicesMap[service].(map[interface{}]interface{}) - found = false - for item := range details { - if DockerComposeImageName == item { - val, itemFound := details[item] - if itemFound { - serviceImageName, ok = val.(string) - if ok { - found = true - break - } - } - } - } - if found { - containersSpec[service.(string)] = ContainerSpec{Name: service.(string), Image: serviceImageName} - } - } - for _, container := range *containers { - solutionName, found := container.GetProjectName() - if !found { - continue - } - serviceName, found := container.GetServiceName() - if !found { - log.Error(err) - continue - } - val, exist := containersSpec[serviceName] - if (exist) && (upgradeInfo.Name == solutionName) { - if container.ImageName() != val.Image { - rc = true - break - } - } - } - } - return rc -} - -func doContainersCheck(solutionInfo *UpstreamResponseUpgradeInfo) { - timeout := time.After(time.Duration(solutionInfo.ThresholdTimeS) * time.Second) - - for solutionInfo.HealthCheckStatus == Undefined { - select { - case <-timeout: - solutionInfo.HealthCheckStatus = Unhealthy - log.Infof("Solution [%s] is Unhealthy", solutionInfo.Name) - return - default: - { - checkContainersCompletedCount := 0 - - for _, healthChekCmd := range solutionInfo.HealthCheckCmds { - container, err := client.GetContainerByName(solutionInfo.Name, healthChekCmd.ContainerName) - if err != nil { - log.Error(err) - break - } - config, err := client.InspectContainer(container) - if err != nil { - log.Error(err) - break - } - if !config.State.Running { - log.Infof("Container %s is NOT Running state.", healthChekCmd.ContainerName) - continue - } - if config.State.Health != nil { - log.Infof("Container %s has embedded healthchek.", healthChekCmd.ContainerName) - if config.State.Health.Status != types.Healthy { - log.Infof("Container %s have healthy information. The current status is [%s]", healthChekCmd.ContainerName, config.State.Health.Status) - continue - } - } else { - log.Infof("Container %s has NOT embedded healthchek. Skip this step.", healthChekCmd.ContainerName) - } - exitCode, err := client.ExecContainer(container, healthChekCmd.Cmd) - if err != nil { - log.Error(err) - break - } - if exitCode == 0 { - log.Infof("Container %s has passed healthcheck command [%s], exit code is [%d]", healthChekCmd.ContainerName, healthChekCmd.Cmd, exitCode) - checkContainersCompletedCount++ - } - } - if checkContainersCompletedCount == len(solutionInfo.HealthCheckCmds) { - log.Infof("Solution [%s] is healthy", solutionInfo.Name) - solutionInfo.HealthCheckStatus = Healthy - return - } - time.Sleep(1 * time.Second) - } - } - } -} - -// doHealthCheck do solutions healthcheck afeter upgrade is completed -func doHealthChek(toCheck map[string]*UpstreamResponseUpgradeInfo) { - for _, item := range toCheck { - doContainersCheck(item) - } -} diff --git a/internal/app/upgradeagent/utils.go b/internal/app/upgradeagent/utils.go index da57e38..3ae1690 100644 --- a/internal/app/upgradeagent/utils.go +++ b/internal/app/upgradeagent/utils.go @@ -5,6 +5,7 @@ import ( "github.com/gofrs/uuid" log "github.com/sirupsen/logrus" + "github.com/urfave/cli" ) // Contains returns true in case if there is item in array. otherwise return false @@ -18,32 +19,37 @@ func Contains(names *[]string, item *string) bool { } // ParseLabels parse labels map to LocalSolutionInfo data structure -func ParseLabels(labels map[string]string) (*LocalSolutionInfo, error) { +func ParseLabels(context *cli.Context, dockerClient DockerClient, labels map[string]string) (DeploymentClient, error) { if value, found := labels[DockerComposeProjectLabel]; found { info := NewLocalSolutionInfo() info.SetDeploymentKind(DockerComposeDeployment) info.SetName(value) info.AddServices(labels[DockerComposeServiceLabel]) - return info, nil + return NewDockerComposeDeployment(context, GetUpstreamClient(context), dockerClient, info), nil } return nil, fmt.Errorf("Cannot read labels [%s]", labels) } // GetLocalSolutionList return the list of running solutions -func GetLocalSolutionList(containers []Container) map[string]*LocalSolutionInfo { - projectMap := make(map[string]*LocalSolutionInfo) +func GetLocalSolutionList(context *cli.Context, dockerClient DockerClient) map[string]DeploymentClient { + projectMap := make(map[string]DeploymentClient) + containers, err := dockerClient.ListContainers() + if err != nil { + log.Error(err) + return projectMap + } if containers != nil { for _, current := range containers { - info, err := ParseLabels(current.Labels()) + deploymentClient, err := ParseLabels(context, dockerClient, current.Labels()) if err != nil { log.Error(err) continue } - if _, ok := projectMap[info.GetName()]; ok { - projectMap[info.GetName()].AddServices(info.GetServices()...) + if _, ok := projectMap[deploymentClient.GetLocalSolutionInfo().GetName()]; ok { + projectMap[deploymentClient.GetLocalSolutionInfo().GetName()].GetLocalSolutionInfo().AddServices(deploymentClient.GetLocalSolutionInfo().GetServices()...) } else { - projectMap[info.GetName()] = info + projectMap[deploymentClient.GetLocalSolutionInfo().GetName()] = deploymentClient } } } @@ -61,3 +67,22 @@ func GenNodeUUID() string { // TBD return "node-uuid" } + +// GetUpstreamClient returns apropriate upstream client instance depend on command line arguments +func GetUpstreamClient(context *cli.Context) UpstreamClient { + switch upstreamType := context.GlobalString(UpstreamTypeName); upstreamType { + case UpstreamTypeValue: + return NewMockUpstreamClient(context) + /* + case UpstreamGitHub: + return NewGitHubUpstreamClient(context) + case UpstreamAmazonS3Bucket: + return NewAmazonS3BucketClient(context) + .... + */ + default: + log.Panicf("unsuported upstreamType [%s]", upstreamType) + } + + return nil +} diff --git a/internal/app/upgradeagent/utils_test.go b/internal/app/upgradeagent/utils_test.go index 21f7b75..bebdb49 100644 --- a/internal/app/upgradeagent/utils_test.go +++ b/internal/app/upgradeagent/utils_test.go @@ -33,24 +33,28 @@ func TestGenNodeUUID(t *testing.T) { } func TestParseLabels(t *testing.T) { - c := CreateTestContainer(t, containerInfo, imageInfo) - info, err := core.ParseLabels(c.Labels()) - assert.NoError(t, err) - assert.Equal(t, projectValue, info.GetName()) - assert.Equal(t, []string{"cache"}, info.GetServices()) - assert.Equal(t, core.DockerComposeDeployment, info.GetDeploymentKind()) - c1 := CreateTestContainer(t, containerInfoNoLabels, imageInfo) - info, err = core.ParseLabels(c1.Labels()) - assert.Error(t, err) - assert.Empty(t, info) + // Should be reimplemented + // + /* c := CreateTestContainer(t, containerInfo, imageInfo) + info, err := core.ParseLabels(c.Labels()) + assert.NoError(t, err) + assert.Equal(t, projectValue, info.GetName()) + assert.Equal(t, []string{"cache"}, info.GetServices()) + assert.Equal(t, core.DockerComposeDeployment, info.GetDeploymentKind()) + c1 := CreateTestContainer(t, containerInfoNoLabels, imageInfo) + info, err = core.ParseLabels(c1.Labels()) + assert.Error(t, err) + assert.Empty(t, info) */ } func TestGetLocalSolutionList(t *testing.T) { - assert.Empty(t, core.GetLocalSolutionList(nil)) - c := CreateTestContainer(t, containerInfoNoLabels, imageInfo) - assert.Empty(t, core.GetLocalSolutionList([]core.Container{*c})) - c = CreateTestContainer(t, containerInfo, imageInfo) - assert.NotEmpty(t, core.GetLocalSolutionList([]core.Container{*c})) - c1 := CreateTestContainer(t, containerInfoNewName, imageInfo) - assert.NotEmpty(t, core.GetLocalSolutionList([]core.Container{*c, *c1})) + // Should be reimplemented + // + /* assert.Empty(t, core.GetLocalSolutionList(nil)) + c := CreateTestContainer(t, containerInfoNoLabels, imageInfo) + assert.Empty(t, core.GetLocalSolutionList([]core.Container{*c})) + c = CreateTestContainer(t, containerInfo, imageInfo) + assert.NotEmpty(t, core.GetLocalSolutionList([]core.Container{*c})) + c1 := CreateTestContainer(t, containerInfoNewName, imageInfo) + assert.NotEmpty(t, core.GetLocalSolutionList([]core.Container{*c, *c1})) */ } From 37baa396e81a6a04a3b9bbb009b9db02280ba3be Mon Sep 17 00:00:00 2001 From: Dmitry Gritsay Date: Tue, 13 Aug 2019 15:50:46 +0300 Subject: [PATCH 3/6] unit test suite has been updated --- cmd/upgradeagent/main.go | 36 +----------- internal/app/upgradeagent/utils.go | 35 ++++++++++++ internal/app/upgradeagent/utils_test.go | 75 ++++++++++++++++++++----- 3 files changed, 96 insertions(+), 50 deletions(-) diff --git a/cmd/upgradeagent/main.go b/cmd/upgradeagent/main.go index d8a3192..912183f 100644 --- a/cmd/upgradeagent/main.go +++ b/cmd/upgradeagent/main.go @@ -12,46 +12,12 @@ func start(context *cli.Context) error { return core.Main(context) } -func setupAppFlags() []cli.Flag { - - return []cli.Flag{ - cli.StringFlag{ - Name: core.HostName, - Usage: core.HostUsage, - Value: core.HostValue, - EnvVar: core.HostEnvVar, - }, - cli.StringFlag{ - Name: core.UpstreamName, - Usage: core.UpstreamUsage, - Value: core.UpstreamValue, - EnvVar: core.UpstreamEnvVar, - }, - cli.StringFlag{ - Name: core.UpgradeIntervalName, - Usage: core.UpgradeIntervalUsage, - Value: core.UpgradeIntervalValue, - EnvVar: core.UpgradeIntervalValueEnvVar, - }, - cli.StringFlag{ - Name: core.UpstreamTypeName, - Usage: core.UpstreamTypeUsage, - Value: core.UpstreamTypeValue, - EnvVar: core.UpstreamTypeValueEnvVar, - }, - cli.BoolFlag{ - Name: core.RunOnceName, - Usage: core.RunOnceUsage, - }, - } -} - func createApp() *cli.App { app := cli.NewApp() app.Name = core.ApplicationName app.Usage = core.ApplicationUsage app.Action = start - app.Flags = setupAppFlags() + app.Flags = core.SetupAppFlags() return app } diff --git a/internal/app/upgradeagent/utils.go b/internal/app/upgradeagent/utils.go index 3ae1690..252c8e2 100644 --- a/internal/app/upgradeagent/utils.go +++ b/internal/app/upgradeagent/utils.go @@ -56,6 +56,41 @@ func GetLocalSolutionList(context *cli.Context, dockerClient DockerClient) map[s return projectMap } +// SetupAppFlags sets array of supported command line arguments +func SetupAppFlags() []cli.Flag { + + return []cli.Flag{ + cli.StringFlag{ + Name: HostName, + Usage: HostUsage, + Value: HostValue, + EnvVar: HostEnvVar, + }, + cli.StringFlag{ + Name: UpstreamName, + Usage: UpstreamUsage, + Value: UpstreamValue, + EnvVar: UpstreamEnvVar, + }, + cli.StringFlag{ + Name: UpgradeIntervalName, + Usage: UpgradeIntervalUsage, + Value: UpgradeIntervalValue, + EnvVar: UpgradeIntervalValueEnvVar, + }, + cli.StringFlag{ + Name: UpstreamTypeName, + Usage: UpstreamTypeUsage, + Value: UpstreamTypeValue, + EnvVar: UpstreamTypeValueEnvVar, + }, + cli.BoolFlag{ + Name: RunOnceName, + Usage: RunOnceUsage, + }, + } +} + // GenUUID generate UUID string func GenUUID() string { u4, _ := uuid.NewV4() diff --git a/internal/app/upgradeagent/utils_test.go b/internal/app/upgradeagent/utils_test.go index bebdb49..7e003c7 100644 --- a/internal/app/upgradeagent/utils_test.go +++ b/internal/app/upgradeagent/utils_test.go @@ -2,11 +2,47 @@ package upgradeagent_test import ( "testing" + "time" + "github.com/docker/docker/api/types" core "github.com/nutanix/patrao/internal/app/upgradeagent" "github.com/stretchr/testify/assert" + "github.com/urfave/cli" ) +type mockDockerClient struct { + t *testing.T + containers []core.Container +} + +func NewMockDockerClient(testing *testing.T, containerList []core.Container) core.DockerClient { + return &mockDockerClient{ + t: testing, + containers: containerList, + } +} + +func (mock mockDockerClient) ListContainers() ([]core.Container, error) { + return mock.containers, nil +} + +func (mock mockDockerClient) StopContainer(core.Container, time.Duration) error { + return nil +} + +func (mock mockDockerClient) ExecContainer(*core.Container, string) (int, error) { + return 0, nil +} + +func (mock mockDockerClient) InspectContainer(*core.Container) (types.ContainerJSON, error) { + var containerJSON types.ContainerJSON + return containerJSON, nil +} + +func (mock mockDockerClient) GetContainerByName(string, string) (*core.Container, error) { + return nil, nil +} + func TestContains(t *testing.T) { array := []string{"one", "two", "three"} @@ -33,28 +69,37 @@ func TestGenNodeUUID(t *testing.T) { } func TestParseLabels(t *testing.T) { - // Should be reimplemented - // - /* c := CreateTestContainer(t, containerInfo, imageInfo) - info, err := core.ParseLabels(c.Labels()) + app := cli.NewApp() + app.Flags = core.SetupAppFlags() + app.Action = func(context *cli.Context) { + c := CreateTestContainer(t, containerInfo, imageInfo) + + deploymentClient, err := core.ParseLabels(context, nil, c.Labels()) assert.NoError(t, err) - assert.Equal(t, projectValue, info.GetName()) - assert.Equal(t, []string{"cache"}, info.GetServices()) - assert.Equal(t, core.DockerComposeDeployment, info.GetDeploymentKind()) + assert.Equal(t, projectValue, deploymentClient.GetLocalSolutionInfo().GetName()) + assert.Equal(t, []string{"cache"}, deploymentClient.GetLocalSolutionInfo().GetServices()) + assert.Equal(t, core.DockerComposeDeployment, deploymentClient.GetLocalSolutionInfo().GetDeploymentKind()) c1 := CreateTestContainer(t, containerInfoNoLabels, imageInfo) - info, err = core.ParseLabels(c1.Labels()) + deploymentClient, err = core.ParseLabels(context, nil, c1.Labels()) assert.Error(t, err) - assert.Empty(t, info) */ + assert.Empty(t, deploymentClient) + } + args := []string{"/Projects/Nutanix/patrao/cmd/upgradeagent/__debug_bin", "--run-once"} + app.Run(args) } func TestGetLocalSolutionList(t *testing.T) { - // Should be reimplemented - // - /* assert.Empty(t, core.GetLocalSolutionList(nil)) + app := cli.NewApp() + app.Flags = core.SetupAppFlags() + app.Action = func(context *cli.Context) { + assert.Empty(t, core.GetLocalSolutionList(context, NewMockDockerClient(t, []core.Container{}))) c := CreateTestContainer(t, containerInfoNoLabels, imageInfo) - assert.Empty(t, core.GetLocalSolutionList([]core.Container{*c})) + assert.Empty(t, core.GetLocalSolutionList(context, NewMockDockerClient(t, []core.Container{*c}))) c = CreateTestContainer(t, containerInfo, imageInfo) - assert.NotEmpty(t, core.GetLocalSolutionList([]core.Container{*c})) + assert.NotEmpty(t, core.GetLocalSolutionList(context, NewMockDockerClient(t, []core.Container{*c}))) c1 := CreateTestContainer(t, containerInfoNewName, imageInfo) - assert.NotEmpty(t, core.GetLocalSolutionList([]core.Container{*c, *c1})) */ + assert.NotEmpty(t, core.GetLocalSolutionList(context, NewMockDockerClient(t, []core.Container{*c, *c1}))) + } + args := []string{"/Projects/Nutanix/patrao/cmd/upgradeagent/__debug_bin", "--run-once"} + app.Run(args) } From f5a2f5a276b1327c5452fdd97215f8c5a66f1abb Mon Sep 17 00:00:00 2001 From: Dmitry Gritsay Date: Wed, 14 Aug 2019 12:33:21 +0300 Subject: [PATCH 4/6] trigger build on CI --- scripts/upgrade_engine/setup-mock.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/upgrade_engine/setup-mock.sh b/scripts/upgrade_engine/setup-mock.sh index fc7fb2e..4ace165 100755 --- a/scripts/upgrade_engine/setup-mock.sh +++ b/scripts/upgrade_engine/setup-mock.sh @@ -22,4 +22,4 @@ curl -v -X PUT "http://localhost:1080/mockserver/expectation" -d '{ # "statusCode" : 200, # "body" : "[{\"ID\": \"container-id\", \"NAME\": \"boosteroid-web_db_1\", \"IMAGE\": \"postgres:10.8\", \"DELETE_VOLUMES\": false}]" # } -#}' \ No newline at end of file +#}' \ No newline at end of file From 34a27267507130fda7155956e27dced555863375 Mon Sep 17 00:00:00 2001 From: Dmitry Gritsay Date: Thu, 5 Sep 2019 17:30:35 +0300 Subject: [PATCH 5/6] refactoring. --- .../upgradeagent/docker_compose_deployment.go | 101 ++++++++---------- internal/app/upgradeagent/dockermanager.go | 42 +++----- .../app/upgradeagent/mock_upstream_client.go | 11 +- internal/app/upgradeagent/upgradeagent.go | 6 +- 4 files changed, 64 insertions(+), 96 deletions(-) diff --git a/internal/app/upgradeagent/docker_compose_deployment.go b/internal/app/upgradeagent/docker_compose_deployment.go index 2e2cc2d..8ef5434 100644 --- a/internal/app/upgradeagent/docker_compose_deployment.go +++ b/internal/app/upgradeagent/docker_compose_deployment.go @@ -1,6 +1,7 @@ package upgradeagent import ( + "fmt" "os" "os/exec" "path" @@ -34,13 +35,16 @@ func NewDockerComposeDeployment(ctx *cli.Context, upstreamServiceClient Upstream // CheckUpgrade do check if there is a new version of the current solution available func (d *dockerComposeDeployment) CheckUpgrade() bool { - upgradeInfo, err := d.upstreamClient.RequestUpgrade(*d.localSolutionInfo) - if err != nil { + var ( + upgradeInfo *UpstreamResponseUpgradeInfo + err error + containers []Container + ) + if upgradeInfo, err = d.upstreamClient.RequestUpgrade(*d.localSolutionInfo); err != nil { log.Error(err) return false } - containers, err := d.dockerClient.ListContainers() - if err != nil { + if containers, err = d.dockerClient.ListContainers(); err != nil { log.Error(err) return false } @@ -54,30 +58,24 @@ func (d *dockerComposeDeployment) CheckUpgrade() bool { // Upgrade does upgrade the current solution func (d *dockerComposeDeployment) DoUpgrade() error { - containers, err := d.dockerClient.ListContainers() - if err != nil { - log.Error(err) - return err + var ( + containers []Container + err error + ) + if containers, err = d.dockerClient.ListContainers(); err != nil { + return fmt.Errorf("DockerComposeDeployment::DoUpgrade [%v]", err) } for _, container := range containers { - name, found := container.GetProjectName() - if !found { - continue - } - if d.localSolutionInfo.name == name { - err := d.dockerClient.StopContainer(container, DefaultTimeoutS*time.Second) - if err != nil { - log.Error(err) + if name, found := container.GetProjectName(); found && d.localSolutionInfo.name == name { + if err = d.dockerClient.StopContainer(container, DefaultTimeoutS*time.Second); err != nil { + return fmt.Errorf("DockerComposeDeployment::DoUpgrade [%v]", err) } } } - err = d.LaunchSolution() - if err != nil { - log.Error(err) - return err + if err = d.LaunchSolution(); err != nil { + return fmt.Errorf("DockerComposeDeployment::DoUpgrade [%v]", err) } log.Infof("Solution [%s] is successful launched", d.localSolutionInfo.name) - return nil } @@ -93,7 +91,6 @@ func (d *dockerComposeDeployment) CheckHealth() bool { default: { checkContainersCompletedCount := 0 - for _, healthChekCmd := range d.upgradeInfo.HealthCheckCmds { container, err := d.dockerClient.GetContainerByName(d.upgradeInfo.Name, healthChekCmd.ContainerName) if err != nil { @@ -147,13 +144,13 @@ func (d *dockerComposeDeployment) GetLocalSolutionInfo() *LocalSolutionInfo { // isNewVersion check if there are new version available. func isNewVersion(upgradeInfo *UpstreamResponseUpgradeInfo, containers []Container) bool { - var rc bool - - rc = false + var ( + found bool + ) containersSpec := make(map[string]ContainerSpec) specMap := make(map[string]interface{}) - err := yaml.Unmarshal([]byte(upgradeInfo.Spec), specMap) - if nil != err { + rc := false + if err := yaml.Unmarshal([]byte(upgradeInfo.Spec), specMap); err != nil { log.Error(err) return false } @@ -162,39 +159,34 @@ func isNewVersion(upgradeInfo *UpstreamResponseUpgradeInfo, containers []Contain for service := range servicesMap { var ( serviceImageName string - found bool ok bool ) details := servicesMap[service].(map[interface{}]interface{}) found = false for item := range details { if DockerComposeImageName == item { - val, itemFound := details[item] - if itemFound { - serviceImageName, ok = val.(string) - if ok { + if val, itemFound := details[item]; itemFound { + if serviceImageName, ok = val.(string); ok { found = true + containersSpec[service.(string)] = ContainerSpec{Name: service.(string), Image: serviceImageName} break } } } } - if found { - containersSpec[service.(string)] = ContainerSpec{Name: service.(string), Image: serviceImageName} - } } for _, container := range containers { - solutionName, found := container.GetProjectName() - if !found { + var ( + solutionName string + serviceName string + ) + if solutionName, found = container.GetProjectName(); !found { continue } - serviceName, found := container.GetServiceName() - if !found { - log.Error(err) + if serviceName, found = container.GetServiceName(); !found { continue } - val, exist := containersSpec[serviceName] - if (exist) && (upgradeInfo.Name == solutionName) { + if val, exist := containersSpec[serviceName]; (exist) && (upgradeInfo.Name == solutionName) { if container.ImageName() != val.Image { rc = true break @@ -210,39 +202,30 @@ func (d *dockerComposeDeployment) LaunchSolution() error { if _, isFileExist := os.Stat(d.upgradeInfo.Name); !os.IsNotExist(isFileExist) { os.RemoveAll(d.upgradeInfo.Name) } - err := os.Mkdir(d.upgradeInfo.Name, os.ModePerm) - if nil != err { - log.Error(err) - return err + + if err := os.Mkdir(d.upgradeInfo.Name, os.ModePerm); err != nil { + return fmt.Errorf("DockerComposeDeployment::LaunchSolution() [%v]", err) } defer os.Remove(d.upgradeInfo.Name) dockerComposeFileName := path.Join(d.upgradeInfo.Name, DockerComposeFileName) f, err := os.Create(dockerComposeFileName) if err != nil { - log.Error(err) - return err + return fmt.Errorf("DockerComposeDeployment::LaunchSolution() [%v]", err) } - defer func() { f.Close() os.Remove(dockerComposeFileName) }() - - _, err = f.Write([]byte(d.upgradeInfo.Spec)) - if err != nil { - log.Error(err) - return err + if _, err = f.Write([]byte(d.upgradeInfo.Spec)); err != nil { + return fmt.Errorf("DockerComposeDeployment::LaunchSolution() [%v]", err) } log.Infof("Launching solution [%s]", d.upgradeInfo.Name) ex, _ := os.Executable() rootPath := filepath.Dir(ex) cmd := exec.Command(DockerComposeCommand, "-f", path.Join(rootPath, dockerComposeFileName), "up", "-d") - err = cmd.Run() - if err != nil { - log.Error(err) - return err + if err = cmd.Run(); err != nil { + return fmt.Errorf("DockerComposeDeployment::LaunchSolution() [%v]", err) } - return nil } diff --git a/internal/app/upgradeagent/dockermanager.go b/internal/app/upgradeagent/dockermanager.go index 6084d02..35fe0e0 100644 --- a/internal/app/upgradeagent/dockermanager.go +++ b/internal/app/upgradeagent/dockermanager.go @@ -1,7 +1,6 @@ package upgradeagent import ( - "errors" "fmt" "strings" "time" @@ -47,16 +46,16 @@ func (client dockerClient) ListContainers() ([]Container, error) { types.ContainerListOptions{}) if err != nil { - return nil, err + return nil, fmt.Errorf("DockerClient::ListContainers() [%v]", err) } for _, runningContainer := range runningContainers { containerInfo, err := client.api.ContainerInspect(bg, runningContainer.ID) if err != nil { - return nil, err + return nil, fmt.Errorf("DockerClient::ListContainers() [%v]", err) } imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image) if err != nil { - return nil, err + return nil, fmt.Errorf("DockerClient::ListContainers() [%v]", err) } c := Container{containerInfo: &containerInfo, imageInfo: &imageInfo} if PatraoAgentContainerName != c.Name() { @@ -71,7 +70,7 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err signal := DefaultStopSignal log.Infof("Stopping %s (%s) with %s", c.Name(), c.ID(), signal) if err := client.api.ContainerKill(bg, c.ID(), signal); err != nil { - return err + return fmt.Errorf("DockerClient::StopContainer() [%v]", err) } // Wait for container to exit, but proceed anyway after the timeout elapses client.waitForStop(c, timeout) @@ -82,12 +81,12 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err if err := client.api.ContainerRemove(bg, c.ID(), types.ContainerRemoveOptions{Force: true, RemoveVolumes: false}); err != nil { - return err + return fmt.Errorf("DockerClient::StopContainer() [%v]", err) } } // Wait for container to be removed. In this case an error is a good thing if err := client.waitForStop(c, timeout); err == nil { - return fmt.Errorf("Container %s (%s) could not be removed", c.Name(), c.ID()) + return fmt.Errorf("DockerClient::StopContainer() [Container %s (%s) could not be removed]", c.Name(), c.ID()) } return nil } @@ -142,16 +141,14 @@ func (client dockerClient) ExecContainer(c *Container, cmd string) (int, error) config := types.ExecConfig{AttachStdin: false, AttachStdout: true, Cmd: cmdWithParams} execID, err := client.api.ContainerExecCreate(ctx, c.ID(), config) if err != nil { - log.Error(err) - return DefaultExitCode, err + return DefaultExitCode, fmt.Errorf("DockerClient::ExecContainer() [%v]", err) } - _, er := client.api.ContainerExecAttach(ctx, execID.ID, types.ExecConfig{}) - if er != nil { - return DefaultExitCode, err + if _, err := client.api.ContainerExecAttach(ctx, execID.ID, types.ExecConfig{}); err != nil { + return DefaultExitCode, fmt.Errorf("DockerClient::ExecContainer() [%v]", err) } err = client.api.ContainerExecStart(ctx, execID.ID, types.ExecStartCheck{}) if err != nil { - return DefaultExitCode, err + return DefaultExitCode, fmt.Errorf("DockerClient::ExecContainer() [%v]", err) } return client.waitForContainerExec(execID.ID, DefaultTimeoutS*time.Second) } @@ -160,21 +157,14 @@ func (client dockerClient) ExecContainer(c *Container, cmd string) (int, error) func (client dockerClient) GetContainerByName(solutionName string, containerName string) (*Container, error) { containers, err := client.ListContainers() if err != nil { - log.Error(err) - return nil, err + return nil, fmt.Errorf("DockerClient::GetContainerByName() [%v]", err) } for _, item := range containers { - currSolutionName, found := item.GetProjectName() - if !found { - continue - } - currServiceName, found := item.GetServiceName() - if !found { - continue - } - if currSolutionName == solutionName && currServiceName == containerName { - return &item, nil + if currSolutionName, found := item.GetProjectName(); found { + if currServiceName, found := item.GetServiceName(); found && currSolutionName == solutionName && currServiceName == containerName { + return &item, nil + } } } - return nil, errors.New("Container not found") + return nil, fmt.Errorf("DockerClient::GetContainerByName() [%s]", "Container not found") } diff --git a/internal/app/upgradeagent/mock_upstream_client.go b/internal/app/upgradeagent/mock_upstream_client.go index 7a2ce7a..20204ec 100644 --- a/internal/app/upgradeagent/mock_upstream_client.go +++ b/internal/app/upgradeagent/mock_upstream_client.go @@ -2,10 +2,10 @@ package upgradeagent import ( "encoding/json" + "fmt" "io/ioutil" "net/http" - log "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -33,23 +33,20 @@ func (mock *mockUpstreamClient) RequestUpgrade(solutionInfo LocalSolutionInfo) ( resp, err := http.Get(currentPath) if nil != err { - log.Error(err) - return NewUpstreamResponseUpgradeInfo(), err + return NewUpstreamResponseUpgradeInfo(), fmt.Errorf("MockUpstreamClient::RequestUpgrade() [%v]", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if nil != err { - log.Error(err) - return NewUpstreamResponseUpgradeInfo(), err + return NewUpstreamResponseUpgradeInfo(), fmt.Errorf("MockUpstreamClient::RequestUpgrade() [%v]", err) } err = json.Unmarshal([]byte(body), &upgradeInfo) if nil != err { - log.Error(err) - return NewUpstreamResponseUpgradeInfo(), err + return NewUpstreamResponseUpgradeInfo(), fmt.Errorf("MockUpstreamClient::RequestUpgrade() [%v]", err) } return upgradeInfo, nil diff --git a/internal/app/upgradeagent/upgradeagent.go b/internal/app/upgradeagent/upgradeagent.go index 1c306aa..6562ac1 100644 --- a/internal/app/upgradeagent/upgradeagent.go +++ b/internal/app/upgradeagent/upgradeagent.go @@ -17,11 +17,9 @@ func Main(context *cli.Context) error { // runOnce do check launched solutions and do upgrade them if there are new versions available func runOnce(context *cli.Context) error { log.Infoln("[+]runOnce()") - localSolutionList := GetLocalSolutionList(context, NewClient()) - for _, current := range localSolutionList { + for _, current := range GetLocalSolutionList(context, NewClient()) { if current.CheckUpgrade() == true { - err := current.DoUpgrade() - if err != nil { + if err := current.DoUpgrade(); err != nil { log.Error(err) current.DoRollback() continue From 9e67db0d8aa5421c3cf94925f5f1bde71259b506 Mon Sep 17 00:00:00 2001 From: Dmitry Gritsay Date: Thu, 5 Sep 2019 17:34:43 +0300 Subject: [PATCH 6/6] unit-test fix --- internal/app/upgradeagent/dockermanager_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/upgradeagent/dockermanager_test.go b/internal/app/upgradeagent/dockermanager_test.go index 104fc90..820dd33 100644 --- a/internal/app/upgradeagent/dockermanager_test.go +++ b/internal/app/upgradeagent/dockermanager_test.go @@ -29,7 +29,7 @@ func TestGetContainerByName(t *testing.T) { assert.NotNil(t, client) c, err := client.GetContainerByName("test", "db") assert.Nil(t, c) - assert.EqualError(t, err, "Container not found") + assert.EqualError(t, err, "DockerClient::GetContainerByName() [Container not found]") } func TestListContainers(t *testing.T) {