Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added the support for --image-name flag in the astro deploy command #1751

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions cmd/software/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ var (

ignoreCacheDeploy = false

EnsureProjectDir = utils.EnsureProjectDir
DeployAirflowImage = deploy.Airflow
DagsOnlyDeploy = deploy.DagsOnlyDeploy
isDagOnlyDeploy bool
description string
EnsureProjectDir = utils.EnsureProjectDir
DeployAirflowImage = deploy.Airflow
DagsOnlyDeploy = deploy.DagsOnlyDeploy
UpdateDeploymentImage = deploy.UpdateDeploymentImage
isDagOnlyDeploy bool
description string
imageName string
runtimeVersionForImageName string
)

var deployExample = `
Expand Down Expand Up @@ -58,6 +61,8 @@ func NewDeployCmd() *cobra.Command {
cmd.Flags().BoolVarP(&ignoreCacheDeploy, "no-cache", "", false, "Do not use cache when building container image")
cmd.Flags().StringVar(&workspaceID, "workspace-id", "", "workspace assigned to deployment")
cmd.Flags().StringVar(&description, "description", "", "Improve traceability by attaching a description to a code deploy. If you don't provide a description, the system automatically assigns a default description based on the deploy type.")
cmd.Flags().StringVar(&imageName, "image-name", "", "Name of the image to deploy. Example - quay.io/astronomer/astro-runtime:12.1.1 or quay.io/astronomer/astro-runtime@sha256:77d85a8b6cd3bdfbcb25cbdb6ffbdb4ac2c1bb390b5951b836451dbdefb6c325")
cmd.Flags().StringVar(&runtimeVersionForImageName, "runtime-version", "", "Runtime version of the image to deploy. Example - 12.1.1. Mandatory if image-name is provided")

if !context.IsCloudContext() && houston.VerifyVersionMatch(houstonVersion, houston.VersionRestrictions{GTE: "0.34.0"}) {
cmd.Flags().BoolVarP(&isDagOnlyDeploy, "dags", "d", false, "Push only DAGs to your Deployment")
Expand Down Expand Up @@ -112,6 +117,9 @@ func deployAirflow(cmd *cobra.Command, args []string) error {
return DagsOnlyDeploy(houstonClient, appConfig, ws, deploymentID, config.WorkingPath, nil, true, description)
}

if imageName != "" || runtimeVersionForImageName != "" {
return UpdateDeploymentImage(houstonClient, deploymentID, ws, runtimeVersionForImageName, imageName)
}
// Since we prompt the user to enter the deploymentID in come cases for DeployAirflowImage, reusing the same deploymentID for DagsOnlyDeploy
deploymentID, err = DeployAirflowImage(houstonClient, config.WorkingPath, deploymentID, ws, byoRegistryDomain, ignoreCacheDeploy, byoRegistryEnabled, forcePrompt, description)
if err != nil {
Expand Down
34 changes: 32 additions & 2 deletions software/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ var (
ErrDagOnlyDeployNotEnabledForDeployment = errors.New("to perform this operation, first set the Deployment type to 'dag_deploy' via the UI or the API or the CLI")
ErrEmptyDagFolderUserCancelledOperation = errors.New("no DAGs found in the dags folder. User canceled the operation")
ErrBYORegistryDomainNotSet = errors.New("Custom registry host is not set in config. It can be set at astronomer.houston.config.deployments.registry.protectedCustomRegistry.updateRegistry.host") //nolint
ErrNoRuntimeVersionPassed = errors.New("no runtime version was provided")
ErrNoImageNamePassed = errors.New("no image name was provided")
)

const (
Expand Down Expand Up @@ -204,14 +206,18 @@ func buildPushDockerImage(houstonClient houston.ClientInterface, c *config.Conte
if byoRegistryEnabled {
runtimeVersion, _ := imageHandler.GetLabel("", runtimeImageLabel)
airflowVersion, _ := imageHandler.GetLabel("", airflowImageLabel)
req := houston.UpdateDeploymentImageRequest{ReleaseName: name, Image: remoteImage, AirflowVersion: airflowVersion, RuntimeVersion: runtimeVersion}
_, err := houston.Call(houstonClient.UpdateDeploymentImage)(req)
_, err := updateDeploymentImageAPICall(houstonClient, remoteImage, name, airflowVersion, runtimeVersion)
return err
}

return nil
}

func updateDeploymentImageAPICall(houstonClient houston.ClientInterface, imageName, releaseName, airflowVersion, runtimeVersion string) (interface{}, error) {
req := houston.UpdateDeploymentImageRequest{ReleaseName: releaseName, Image: imageName, AirflowVersion: airflowVersion, RuntimeVersion: runtimeVersion}
return houston.Call(houstonClient.UpdateDeploymentImage)(req)
}

func validAirflowImageRepo(image string) bool {
validDockerfileBaseImages := map[string]bool{
"quay.io/astronomer/ap-airflow": true,
Expand Down Expand Up @@ -427,3 +433,27 @@ func DagsOnlyDeploy(houstonClient houston.ClientInterface, appConfig *houston.Ap
}
return fileutil.UploadFile(&uploadFileArgs)
}

func UpdateDeploymentImage(houstonClient houston.ClientInterface, deploymentID, wsID, runtimeVersion, imageName string) error {
if runtimeVersion == "" {
return ErrNoRuntimeVersionPassed
}
Comment on lines +438 to +440
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do we need to validate that runtimeVersion is a valid semver and present in updates.astronomer.io? Or does the Houston API do that for us?

if imageName == "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Should we validate that the image being passed is built using the same runtime version as the one being passed on as user input? Or does the Houston API do that for us?

return ErrNoImageNamePassed
}
deploymentIDForCurrentCmd, _, err := getDeploymentIDForCurrentCommandVar(houstonClient, wsID, deploymentID, deploymentID == "")
if err != nil {
return err
}
deploymentID = deploymentIDForCurrentCmd
if deploymentID == "" {
return errInvalidDeploymentID
}
Comment on lines +444 to +451
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
deploymentIDForCurrentCmd, _, err := getDeploymentIDForCurrentCommandVar(houstonClient, wsID, deploymentID, deploymentID == "")
if err != nil {
return err
}
deploymentID = deploymentIDForCurrentCmd
if deploymentID == "" {
return errInvalidDeploymentID
}
deploymentID, _, err := getDeploymentIDForCurrentCommandVar(houstonClient, wsID, deploymentID, deploymentID == "")
if err != nil {
return err
}
if deploymentID == "" {
return errInvalidDeploymentID
}

nit: simplifying the logic

deploymentInfo, err := houston.Call(houstonClient.GetDeployment)(deploymentID)
if err != nil {
return fmt.Errorf("failed to get deployment info: %w", err)
}
_, err = updateDeploymentImageAPICall(houstonClient, imageName, deploymentInfo.ReleaseName, "", runtimeVersion)
fmt.Println("Image successfully updated")
return err
}
80 changes: 80 additions & 0 deletions software/deploy/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -841,3 +841,83 @@ func (s *Suite) TestDeployDagsOnlyFailure() {
s.True(os.IsNotExist(err))
})
}

func (s *Suite) TestUpdateDeploymentImage() {
testUtil.InitTestConfig(testUtil.SoftwarePlatform)
deploymentID := "test-deployment-id"
wsID := "test-workspace-id"
runtimeVersion := "12.1.1"
imageName := "imageName"
releaseName := "releaseName"

s.Run("When runtimeVersion is empty", func() {
houstonMock := new(houston_mocks.ClientInterface)
err := UpdateDeploymentImage(houstonMock, deploymentID, wsID, "", imageName)
s.ErrorIs(err, ErrNoRuntimeVersionPassed)
houstonMock.AssertExpectations(s.T())
})

s.Run("When imageName is empty", func() {
houstonMock := new(houston_mocks.ClientInterface)
err := UpdateDeploymentImage(houstonMock, deploymentID, wsID, runtimeVersion, "")
s.ErrorIs(err, ErrNoImageNamePassed)
houstonMock.AssertExpectations(s.T())
})

s.Run("When getDeploymentIDForCurrentCommandVar gives an error", func() {
getDeploymentIDForCurrentCommandVar = func(houstonClient houston.ClientInterface, wsID, deploymentID string, prompt bool) (string, []houston.Deployment, error) {
return deploymentID, nil, errDeploymentNotFound
}
houstonMock := new(houston_mocks.ClientInterface)
err := UpdateDeploymentImage(houstonMock, deploymentID, wsID, runtimeVersion, imageName)
s.ErrorIs(err, errDeploymentNotFound)
houstonMock.AssertExpectations(s.T())
})

s.Run("When an error occurs in the GetDeployment api call", func() {
getDeploymentIDForCurrentCommandVar = func(houstonClient houston.ClientInterface, wsID, deploymentID string, prompt bool) (string, []houston.Deployment, error) {
return deploymentID, nil, nil
}
houstonMock := new(houston_mocks.ClientInterface)
houstonMock.On("GetDeployment", mock.Anything).Return(nil, errMockHouston).Once()

err := UpdateDeploymentImage(houstonMock, deploymentID, wsID, runtimeVersion, imageName)
s.ErrorContains(err, "failed to get deployment info: some houston error")
houstonMock.AssertExpectations(s.T())
})

s.Run("Houston API call throws error", func() {
getDeploymentIDForCurrentCommandVar = func(houstonClient houston.ClientInterface, wsID, deploymentID string, prompt bool) (string, []houston.Deployment, error) {
return deploymentID, nil, nil
}
houstonMock := new(houston_mocks.ClientInterface)
deployment := &houston.Deployment{
ReleaseName: releaseName,
}
houstonMock.On("GetDeployment", mock.Anything).Return(deployment, nil).Once()
houstonMock.On("UpdateDeploymentImage", mock.Anything).Return(nil, errMockHouston).Once()
err := UpdateDeploymentImage(houstonMock, deploymentID, wsID, runtimeVersion, imageName)
s.ErrorContains(err, "some houston error")
houstonMock.AssertExpectations(s.T())
})

s.Run("Successful API call", func() {
getDeploymentIDForCurrentCommandVar = func(houstonClient houston.ClientInterface, wsID, deploymentID string, prompt bool) (string, []houston.Deployment, error) {
return deploymentID, nil, nil
}
houstonMock := new(houston_mocks.ClientInterface)
updateDeploymentImageResp := &houston.UpdateDeploymentImageResp{
ReleaseName: releaseName,
AirflowVersion: "",
RuntimeVersion: runtimeVersion,
}
deployment := &houston.Deployment{
ReleaseName: releaseName,
}
houstonMock.On("GetDeployment", mock.Anything).Return(deployment, nil).Once()
houstonMock.On("UpdateDeploymentImage", mock.Anything).Return(updateDeploymentImageResp, nil).Once()
err := UpdateDeploymentImage(houstonMock, deploymentID, wsID, runtimeVersion, imageName)
houstonMock.AssertExpectations(s.T())
s.ErrorIs(err, nil)
})
}