internal/apiclient/bundles.go (247 lines of code) (raw):
// Copyright 2020 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
//
// http://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 apiclient
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"internal/clilog"
"io"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"cloud.google.com/go/storage"
)
// entityPayloadList stores list of entities
var entityPayloadList [][]byte // types.EntityPayloadList
// WriteByteArrayToFile accepts []bytes and writes to a file
func WriteByteArrayToFile(exportFile string, fileAppend bool, payload []byte) error {
fileFlags := os.O_CREATE | os.O_WRONLY
if fileAppend {
fileFlags |= os.O_APPEND
} else {
fileFlags |= os.O_TRUNC
}
f, err := os.OpenFile(exportFile, fileFlags, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(payload)
if err != nil {
clilog.Error.Println("error writing to file: ", err)
return err
}
return nil
}
// WriteArrayByteArrayToFile accepts [][]bytes and writes to a file
func WriteArrayByteArrayToFile(exportFile string, fileAppend bool, payload [][]byte) error {
fileFlags := os.O_CREATE | os.O_WRONLY
if fileAppend {
fileFlags |= os.O_APPEND
}
f, err := os.OpenFile(exportFile, fileFlags, 0o644)
if err != nil {
return err
}
defer f.Close()
// begin json array
_, err = f.Write([]byte("["))
if err != nil {
clilog.Error.Println("error writing to file ", err)
return err
}
payloadFromArray := bytes.Join(payload, []byte(","))
// add json array terminate
payloadFromArray = append(payloadFromArray, byte(']'))
_, err = f.Write(payloadFromArray)
if err != nil {
clilog.Error.Println("error writing to file: ", err)
return err
}
return nil
}
func FolderExists(folder string) (err error) {
if folder == "" {
return nil
}
_, err = os.Stat(folder)
if err != nil {
return fmt.Errorf("folder not found or write permission denied")
}
return nil
}
func SetEntityPayloadList(respBody []byte) {
entityPayloadList = append(entityPayloadList, respBody)
}
func GetEntityPayloadList() [][]byte {
return entityPayloadList
}
func ClearEntityPayloadList() {
entityPayloadList = entityPayloadList[:0]
}
func ExtractTgz(gcsURL string) (folder string, err error) {
ctx := context.Background()
folder, err = os.MkdirTemp("", "integration")
if err != nil {
return "", err
}
// Parse the GCS URL
parsedURL, err := url.Parse(gcsURL)
if err != nil {
return "", fmt.Errorf("Error parsing GCS URL:", err)
}
if parsedURL.Scheme != "gs" {
return "", fmt.Errorf("Invalid GCS URL scheme. Should be 'gs://'")
}
bucketName := parsedURL.Host
objectName := strings.TrimPrefix(parsedURL.Path, "/")
fileName := filepath.Base(gcsURL)
// Create a Google Cloud Storage client
client, err := storage.NewClient(ctx)
if err != nil {
return "", fmt.Errorf("Error creating GCS client:", err)
}
defer client.Close()
// Get a handle to the bucket and the object
bucket := client.Bucket(bucketName)
object := bucket.Object(objectName)
// Create a reader to stream the object's content
reader, err := object.NewReader(ctx)
if err != nil {
return "", fmt.Errorf("Error creating object reader:", err)
}
defer reader.Close()
// Create the local file to save the download
localFile, err := os.Create(path.Join(folder, fileName))
if err != nil {
return "", fmt.Errorf("Error creating local file:", err)
}
defer localFile.Close()
// Download the object and save it to the local file
if _, err := io.Copy(localFile, reader); err != nil {
return "", fmt.Errorf("Error downloading object:", err)
}
// Open the .tgz file
file, err := os.Open(path.Join(folder, fileName))
if err != nil {
return "", fmt.Errorf("Error opening file:", err)
}
defer file.Close() // Ensure file closure
// Create a gzip reader
gzipReader, err := gzip.NewReader(file)
if err != nil {
return "", fmt.Errorf("Error creating gzip reader:", err)
}
defer gzipReader.Close() // Ensure closure
// Create a tar reader
tarReader := tar.NewReader(gzipReader)
// Extract each file from the tar archive
for {
header, err := tarReader.Next()
if err == io.EOF {
break // End of archive
}
if err != nil {
return "", fmt.Errorf("Error reading tar entry:", err)
}
if strings.Contains(header.Name, "..") {
continue
}
// Process the file header
switch header.Typeflag {
case tar.TypeDir:
// Create directory
if err := os.Mkdir(path.Join(folder, header.Name), 0o755); err != nil {
return "", fmt.Errorf("Error creating directory:", err)
}
case tar.TypeReg:
// Create output file
outFile, err := os.Create(path.Join(folder, header.Name))
if err != nil {
return "", fmt.Errorf("Error creating file:", err)
}
defer outFile.Close()
// Copy contents from the tar to the output file
if _, err := io.Copy(outFile, tarReader); err != nil {
return "", fmt.Errorf("Error writing file:", err)
}
default:
return "", fmt.Errorf("Unsupported type: %b in %s\n", header.Typeflag, header.Name)
}
}
return folder, nil
}
func GetCloudDeployGCSLocations(cloudDeployProjectId string, cloudDeployLocation string,
pipeline string, release string) (skaffoldConfigUri string, err error) {
type cloudDeployRelease struct {
SkaffoldConfigUri string `json:"skaffoldConfigUri"`
TargetArtifacts map[string]struct {
SkaffoldConfigPath string `json:"skaffoldConfigPath"`
ManifestPath string `json:"manifestPath"`
ArtifactUri string `json:"artifactUri"`
PhaseArtifacts map[string]struct {
SkaffoldConfigPath string `json:"skaffoldConfigPath"`
ManifestPath string `json:"manifestPath"`
} `json:"phaseArtifacts"`
} `json:"targetArtifacts"`
}
r := cloudDeployRelease{}
cloudDeployURL := fmt.Sprintf("https://clouddeploy.googleapis.com/v1/projects/%s/locations/%s/deliveryPipelines/%s/releases/%s",
cloudDeployProjectId, cloudDeployLocation, pipeline, release)
u, _ := url.Parse(cloudDeployURL)
ClientPrintHttpResponse.Set(false)
respBody, err := HttpClient(u.String())
if err != nil {
return "", err
}
defer ClientPrintHttpResponse.Set(GetCmdPrintHttpResponseSetting())
err = json.Unmarshal(respBody, &r)
if err != nil {
return "", err
}
return r.SkaffoldConfigUri, nil
}
func WriteResultsFile(deployOutputGCS string, status string) (err error) {
contents := fmt.Sprintf("{\"resultStatus\": \"%s\"}", status)
filename := "results.json"
err = writeGCSFile(deployOutputGCS, filename, contents)
if err != nil {
return err
}
return nil
}
func parseGCSURI(gcsURI string) (bucketName, objectPath string, err error) {
// Parse the GCS URL
parsedURL, err := url.Parse(gcsURI)
if err != nil {
return "", "", fmt.Errorf("Error parsing GCS URL:", err)
}
if parsedURL.Scheme != "gs" {
return "", "", fmt.Errorf("Invalid GCS URL scheme. Should be 'gs://'")
}
// Remove the protocol prefix
uri := strings.TrimPrefix(gcsURI, `gs://`)
// Split based on the first '/'
parts := strings.SplitN(uri, "/", 2)
// Check for proper URI format
if len(parts) != 2 {
return "", "", fmt.Errorf("Invalid GCS URI format")
}
return parts[0], parts[1], nil
}
func WriteManifest(deployOutputGCS string, version string) (err error) {
manifestFile := "manifest.txt"
resultsFile := "results.json"
manifestContents := "integrationcli manifest rendered content using version " + version
// do not use path.Join. This will caseu gs:// to be written as gs:/ and failed the release.
resultContents := fmt.Sprintf(`{"resultStatus": "SUCCEEDED", "manifestFile": "%s"}`, deployOutputGCS+"/"+manifestFile)
err = writeGCSFile(deployOutputGCS, manifestFile, manifestContents)
if err != nil {
return err
}
err = writeGCSFile(deployOutputGCS, resultsFile, resultContents)
if err != nil {
return err
}
return nil
}
func writeGCSFile(deployOutputGCS string, fileName string, contents string) (err error) {
ctx := context.Background()
client, err := storage.NewClient(ctx)
if err != nil {
return fmt.Errorf("storage.NewClient: %v", err)
}
defer client.Close()
// Extract bucket name and object path from GCS URI
bucketName, objectPath, err := parseGCSURI(deployOutputGCS)
objectName := path.Join(objectPath, fileName)
bucket := client.Bucket(bucketName)
object := bucket.Object(objectName)
writer := object.NewWriter(ctx)
// Write the content
if _, err := writer.Write([]byte(contents)); err != nil {
return fmt.Errorf("Object(%q).NewWriter: %v", objectName, err)
}
// Close the writer to ensure data is uploaded
if err := writer.Close(); err != nil {
return fmt.Errorf("Writer.Close: %v", err)
}
return nil
}