custom-targets/infrastructure-manager/im-deployer/inframanager.go (128 lines of code) (raw):
// Copyright 2023 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// https://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"errors"
"fmt"
"strings"
"time"
config "cloud.google.com/go/config/apiv1"
"cloud.google.com/go/config/apiv1/configpb"
retry "github.com/avast/retry-go/v4"
)
// getDeployment gets the Deployment.
func getDeployment(ctx context.Context, client *config.Client, deploymentName string) (*configpb.Deployment, error) {
req := &configpb.GetDeploymentRequest{
Name: deploymentName,
}
return client.GetDeployment(ctx, req)
}
// pollDeploymentUntilTerminal repeatedly calls GetDeployment until all retry attempts are consumed or the Deployment
// reaches a terminal state. If the latest revision provided changes on the Deployment while polling then an error
// is returned.
func pollDeploymentUntilTerminal(ctx context.Context, client *config.Client, deploymentName string, latestRevision string) (*configpb.Deployment, error) {
attempts := 0
dep, err := retry.DoWithData(
func() (*configpb.Deployment, error) {
attempts++
dep, err := getDeployment(ctx, client, deploymentName)
if err != nil {
return nil, err
}
if dep.LatestRevision != latestRevision {
return nil, fmt.Errorf("latest revision changed from %s to %s", latestRevision, dep.LatestRevision)
}
state := dep.State
fmt.Printf("Deployment %s state is %s\n", deploymentName, state.String())
if isSucceededDeployment(state) || isFailedDeployment(state) {
return dep, nil
} else if isInProgressDeployment(state) {
return nil, errors.New("deployment still in progress")
}
return nil, fmt.Errorf("unknown deployment state %s", state)
},
// Keep retrying only if Deployment was retrieved and is still in progress.
retry.RetryIf(func(err error) bool {
return err.Error() == "deployment still in progress"
}),
retry.Attempts(20),
retry.Delay(30*time.Second),
)
if err != nil {
return nil, fmt.Errorf("error polling deployment until terminal state after %d attempts: %v", attempts, err)
}
return dep, nil
}
// createDeployment creates the Deployment and waits for the LRO to complete. While waiting for the LRO
// to complete the Deployment is periodically retrieved in order to log a state update.
func createDeployment(ctx context.Context, client *config.Client, deployment *configpb.Deployment) (*configpb.Deployment, error) {
// Name is "projects/{project}/locations/{location}/deployments/{deployment}".
nameParts := strings.Split(deployment.Name, "/")
op, err := client.CreateDeployment(ctx, &configpb.CreateDeploymentRequest{
Parent: fmt.Sprintf("projects/%s/locations/%s", nameParts[1], nameParts[3]),
DeploymentId: nameParts[5],
Deployment: deployment,
})
if err != nil {
return nil, fmt.Errorf("error creating infrastructure manager deployment: %v", err)
}
fmt.Printf("Waiting on create Deployment operation %s\n", op.Name())
var d *configpb.Deployment
for {
time.Sleep(30 * time.Second)
pd, err := op.Poll(ctx)
if err != nil {
return nil, fmt.Errorf("error polling create deployment operation: %v", err)
}
if pd != nil {
d = pd
break
}
// If the operation isn't complete then get the Deployment to log the current state.
tempD, err := getDeployment(ctx, client, deployment.Name)
if err != nil {
return nil, fmt.Errorf("error getting deployment: %v", err)
}
fmt.Printf("Create operation still in progress, current Deployment state: %s\n", tempD.State)
}
return d, nil
}
// updateDeployment updates the Deployment and waits for the LRO to complete. While waiting for the LRO
// to complete the Deployment is periodically retrieved in order to log a state update.
func updateDeployment(ctx context.Context, client *config.Client, renderedDeployment *configpb.Deployment) (*configpb.Deployment, error) {
op, err := client.UpdateDeployment(ctx, &configpb.UpdateDeploymentRequest{
Deployment: renderedDeployment,
})
if err != nil {
return nil, fmt.Errorf("error calling update deployment: %v", err)
}
fmt.Printf("Waiting on update Deployment operation %s\n", op.Name())
var d *configpb.Deployment
for {
time.Sleep(30 * time.Second)
pd, err := op.Poll(ctx)
if err != nil {
return nil, fmt.Errorf("error polling create deployment operation: %v", err)
}
if pd != nil {
d = pd
break
}
// If the operation isn't complete then get the Deployment to log the current state.
tempD, err := getDeployment(ctx, client, renderedDeployment.Name)
if err != nil {
return nil, fmt.Errorf("error getting deployment: %v", err)
}
fmt.Printf("Update operation still in progress, current Deployment state: %s", tempD.State)
}
return d, nil
}
// isInProgressDeployment returns whether the Deployment state is considered to be in progress by the deployer.
func isInProgressDeployment(state configpb.Deployment_State) bool {
return state == configpb.Deployment_CREATING || state == configpb.Deployment_UPDATING
}
// isSucceededDeployment returns whether the Deployment state is considered to be succeeded by the deployer.
func isSucceededDeployment(state configpb.Deployment_State) bool {
return state == configpb.Deployment_ACTIVE
}
// isFailedDeployment returns whether the Deployment state is considered to be failed by the deployer.
func isFailedDeployment(state configpb.Deployment_State) bool {
switch state {
case configpb.Deployment_FAILED,
configpb.Deployment_SUSPENDED,
configpb.Deployment_DELETED,
configpb.Deployment_DELETING:
return true
}
return false
}
// getRevision gets the Revision.
func getRevision(ctx context.Context, client *config.Client, revisionName string) (*configpb.Revision, error) {
req := &configpb.GetRevisionRequest{
Name: revisionName,
}
return client.GetRevision(ctx, req)
}