talos/hack/cloud-image-uploader/gcp.go
Andrey Smirnov 7f0300f108
feat: update dependencies, Kubernetes 1.34.0-alpha.2
Bump all dependencies, many small changes due to new golangci-lint
version.

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
2025-06-30 19:05:22 +04:00

308 lines
7.8 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"cloud.google.com/go/storage"
"github.com/google/uuid"
"github.com/siderolabs/go-retry/retry"
"golang.org/x/sync/errgroup"
"google.golang.org/api/compute/v1"
"google.golang.org/api/googleapi"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
)
// GCPUploder registers the image in GCP.
type GCPUploder struct {
Options Options
storageClient *storage.Client
computeService *compute.Service
projectID string
imagePath string
}
// NewGCPUploder creates a new GCPUploder.
func NewGCPUploder(options Options) (*GCPUploder, error) {
projectID := os.Getenv("GOOGLE_PROJECT_ID")
credentials := os.Getenv("GOOGLE_CREDENTIALS")
if projectID == "" {
return nil, fmt.Errorf("gcp: GOOGLE_PROJECT_ID is not set")
}
if credentials == "" {
return nil, fmt.Errorf("gcp: GOOGLE_CREDENTIALS is not set")
}
gcpUploader := &GCPUploder{
Options: options,
}
gcpUploader.projectID = projectID
var err error
gcpUploader.storageClient, err = storage.NewClient(context.Background(), option.WithCredentialsJSON([]byte(credentials)))
if err != nil {
return nil, fmt.Errorf("gcp: failed to create google storage client: %w", err)
}
gcpUploader.computeService, err = compute.NewService(context.Background(), option.WithCredentialsJSON([]byte(credentials)))
if err != nil {
return nil, fmt.Errorf("gcp: failed to create google compute service: %w", err)
}
return gcpUploader, nil
}
// Upload uploads the image to GCP.
func (u *GCPUploder) Upload(ctx context.Context) error {
bucketName := fmt.Sprintf("talos-image-upload-%s", uuid.New())
bucketHandle := u.storageClient.Bucket(bucketName)
if err := bucketHandle.Create(ctx, u.projectID, &storage.BucketAttrs{
PublicAccessPrevention: storage.PublicAccessPreventionEnforced,
}); err != nil {
return fmt.Errorf("gcp: failed to create bucket %s: %w", bucketName, err)
}
log.Println("gcp: created bucket", bucketName)
defer func() {
objects := bucketHandle.Objects(ctx, nil)
for {
objAttr, err := objects.Next()
if errors.Is(err, iterator.Done) {
break
}
if err != nil {
log.Printf("gcp: failed to list objects: %v", err)
}
if err := bucketHandle.Object(objAttr.Name).Delete(ctx); err != nil {
log.Printf("gcp: failed to delete object %s: %v", objAttr.Name, err)
}
}
if err := bucketHandle.Delete(ctx); err != nil {
log.Printf("gcp: failed to delete bucket %s: %v", bucketName, err)
}
}()
var g errgroup.Group
for _, arch := range u.Options.Architectures {
g.Go(func() error {
return u.uploadImage(ctx, arch, bucketName)
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf("gcp: failed to upload images: %w", err)
}
return nil
}
func (u *GCPUploder) uploadImage(ctx context.Context, arch, bucketName string) error {
objectPath := u.Options.GCPImage(arch)
objectName := filepath.Base(objectPath)
objectReader, err := os.Open(objectPath)
if err != nil {
return fmt.Errorf("gcp: failed to open object data file %s: %w", objectPath, err)
}
objectHandle := u.storageClient.Bucket(bucketName).Object(objectName)
objectWriter := objectHandle.NewWriter(ctx)
defer objectWriter.Close() //nolint:errcheck
if _, err := io.Copy(objectWriter, objectReader); err != nil {
return fmt.Errorf("gcp: failed to write object data: %w", err)
}
if err := objectWriter.Close(); err != nil {
return fmt.Errorf("gcp: failed to close object writer: %w", err)
}
u.imagePath = fmt.Sprintf("https://storage.googleapis.com/%s/%s", bucketName, objectName)
log.Println("gcp: uploaded image", u.imagePath)
return u.registerImage(arch)
}
//nolint:gocyclo
func (u *GCPUploder) registerImage(arch string) error {
imageName := fmt.Sprintf("talos-%s-%s", strings.ReplaceAll(u.Options.Tag, ".", "-"), arch)
if u.Options.NamePrefix != "" {
imageName = fmt.Sprintf("%s-talos-%s-%s", u.Options.NamePrefix, strings.ReplaceAll(u.Options.Tag, ".", "-"), arch)
}
exists, err := u.checkImageExists(imageName)
if err != nil {
return err
}
if exists {
log.Printf("gcp: image %s already exists, deleting", imageName)
if deleteErr := u.deleteImage(imageName); deleteErr != nil {
return deleteErr
}
}
operationID, link, err := u.insertImage(imageName, arch)
if err != nil {
return err
}
if err := retry.Constant(15*time.Minute, retry.WithUnits(30*time.Second)).Retry(func() error {
op, err := u.computeService.GlobalOperations.Get(u.projectID, operationID).Do()
if err != nil {
return fmt.Errorf("gcp: failed to get operation: %w", err)
}
if op.HTTPStatusCode != http.StatusOK {
return fmt.Errorf("gcp: operation failed with http error message: %s", op.HttpErrorMessage)
}
if op.Error != nil {
return fmt.Errorf("gcp: operation faild with error message: %s", op.Error.Errors[0].Message)
}
if op.Status == "DONE" {
return nil
}
log.Printf("gcp: image creation progress: %d", op.Progress)
return retry.ExpectedError(fmt.Errorf("gcp: image status is %s", op.Status))
}); err != nil {
return fmt.Errorf("gcp: image creation is taking longer than expected: %w", err)
}
pushResult(CloudImage{
Cloud: "gcp",
Tag: u.Options.Tag,
Region: "us",
Arch: arch,
Type: "compute#image",
ID: link,
})
return nil
}
func (u *GCPUploder) checkImageExists(imageName string) (bool, error) {
_, err := u.computeService.Images.Get(u.projectID, imageName).Do()
if err != nil {
var googleErr *googleapi.Error
if errors.As(err, &googleErr) {
if googleErr.Code == http.StatusNotFound {
return false, nil
}
}
return false, fmt.Errorf("gcp: failed to get image %s: %w", imageName, err)
}
return true, nil
}
func (u *GCPUploder) insertImage(imageName, arch string) (operationID, imageLink string, err error) {
var archImage string
switch arch {
case "amd64":
archImage = "x86_64"
case "arm64":
archImage = "ARM64"
default:
return "", "", fmt.Errorf("gcp: unknown architecture %s", arch)
}
op, err := u.computeService.Images.Insert(u.projectID, &compute.Image{
Architecture: archImage,
Description: fmt.Sprintf("Talos %s %s", u.Options.Tag, arch),
GuestOsFeatures: []*compute.GuestOsFeature{
{
Type: "VIRTIO_SCSI_MULTIQUEUE",
},
{
Type: "UEFI_COMPATIBLE",
},
},
Name: imageName,
RawDisk: &compute.ImageRawDisk{
Source: u.imagePath,
},
ShieldedInstanceInitialState: &compute.InitialStateConfig{},
}).Do()
if err != nil {
return "", "", fmt.Errorf("gcp: failed to insert image: %w", err)
}
if op.HTTPStatusCode != http.StatusOK {
return "", "", fmt.Errorf("gcp: insert image failed with http error message: %s", op.HttpErrorMessage)
}
if op.Error != nil {
return "", "", fmt.Errorf("gcp: insert image failed with error message: %s", op.Error.Errors[0].Message)
}
log.Printf("gcp: image %s is being created with operation %s", imageName, op.Name)
return op.Name, op.TargetLink, nil
}
func (u *GCPUploder) deleteImage(imageName string) error {
if _, err := u.computeService.Images.Delete(u.projectID, imageName).Do(); err != nil {
return fmt.Errorf("gcp: failed to delete image %s: %w", imageName, err)
}
if err := retry.Constant(5*time.Minute, retry.WithUnits(30*time.Second)).Retry(func() error {
_, err := u.computeService.Images.Get(u.projectID, imageName).Do()
if err != nil {
var googleErr *googleapi.Error
if errors.As(err, &googleErr) {
if googleErr.Code == http.StatusNotFound {
return nil
}
}
return err
}
return retry.ExpectedError(fmt.Errorf("gcp: image %s still exists", imageName))
}); err != nil {
return fmt.Errorf("gcp: failed to delete image %s: %w", imageName, err)
}
return nil
}