github.com/go-goxm/goxm@v0.4.4/codeartifact.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"crypto/sha256"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"path"
    11  	"strings"
    12  
    13  	"github.com/aws/aws-sdk-go-v2/aws"
    14  	awsconfig "github.com/aws/aws-sdk-go-v2/config"
    15  	"github.com/aws/aws-sdk-go-v2/service/codeartifact"
    16  	codeartifactTypes "github.com/aws/aws-sdk-go-v2/service/codeartifact/types"
    17  	"golang.org/x/exp/slices"
    18  )
    19  
    20  type CodeArtifactClient interface {
    21  	ListPackageVersions(
    22  		ctx context.Context,
    23  		params *codeartifact.ListPackageVersionsInput,
    24  		optFns ...func(*codeartifact.Options),
    25  	) (*codeartifact.ListPackageVersionsOutput, error)
    26  
    27  	GetPackageVersionAsset(
    28  		ctx context.Context,
    29  		params *codeartifact.GetPackageVersionAssetInput,
    30  		optFns ...func(*codeartifact.Options),
    31  	) (*codeartifact.GetPackageVersionAssetOutput, error)
    32  
    33  	PublishPackageVersion(
    34  		ctx context.Context,
    35  		params *codeartifact.PublishPackageVersionInput,
    36  		optFns ...func(*codeartifact.Options),
    37  	) (*codeartifact.PublishPackageVersionOutput, error)
    38  }
    39  
    40  type CodeArtifactRepoConfig struct {
    41  	RepoTypeConfig
    42  	Domain      *string `json:"domain"`
    43  	Namespace   *string `json:"namespace"`
    44  	Repository  *string `json:"repository"`
    45  	DomainOwner *string `json:"domain_owner"`
    46  	Publish     bool    `json:"publish"`
    47  
    48  	client CodeArtifactClient
    49  }
    50  
    51  func (r *CodeArtifactRepoConfig) Get(ctx context.Context, module, attifact string) (io.ReadCloser, int, error) {
    52  	if attifact == "@latest" {
    53  		return nil, http.StatusNotFound, fmt.Errorf("Not implemented: %v/%v", module, attifact)
    54  	}
    55  
    56  	pkg := codeArtPackageEscape(module)
    57  	namespace := codeArtNamespaceDefault(r.Namespace)
    58  
    59  	client, err := r.getClient(ctx)
    60  	if err != nil {
    61  		return nil, http.StatusForbidden, err
    62  	}
    63  
    64  	if attifact == "@v/list" {
    65  		input := &codeartifact.ListPackageVersionsInput{
    66  			Package:     aws.String(pkg),
    67  			Domain:      r.Domain,
    68  			Namespace:   namespace,
    69  			Repository:  r.Repository,
    70  			DomainOwner: r.DomainOwner,
    71  			Format:      codeartifactTypes.PackageFormatGeneric,
    72  			Status:      codeartifactTypes.PackageVersionStatusPublished,
    73  			MaxResults:  aws.Int32(50),
    74  		}
    75  
    76  		output, err := client.ListPackageVersions(ctx, input)
    77  		if err != nil {
    78  			return nil, http.StatusNotFound, fmt.Errorf("Error listing CodeArtifact versions: %v: %w", codeArtListVersionsString(input), err)
    79  		}
    80  		logf("Got CodeArtifact versions: %v Count:%d", codeArtListVersionsString(input), len(output.Versions))
    81  
    82  		buf := bytes.NewBuffer(nil)
    83  		for _, version := range output.Versions {
    84  			fmt.Fprintf(buf, "%v\n", aws.ToString(version.Version))
    85  		}
    86  
    87  		return io.NopCloser(buf), 0, nil
    88  	}
    89  
    90  	asset := attifact[3:]
    91  	assetExt := path.Ext(asset)
    92  
    93  	if !slices.Contains([]string{".info", ".mod", ".zip"}, assetExt) {
    94  		return nil, http.StatusForbidden, fmt.Errorf("Asset extension not supported: %v/%v", module, attifact)
    95  	}
    96  
    97  	version := asset[:len(asset)-len(assetExt)]
    98  
    99  	input := &codeartifact.GetPackageVersionAssetInput{
   100  		Asset:          aws.String(asset),
   101  		Package:        aws.String(pkg),
   102  		PackageVersion: aws.String(version),
   103  		Domain:         r.Domain,
   104  		Namespace:      namespace,
   105  		Repository:     r.Repository,
   106  		DomainOwner:    r.DomainOwner,
   107  		Format:         codeartifactTypes.PackageFormatGeneric,
   108  	}
   109  
   110  	output, err := client.GetPackageVersionAsset(ctx, input)
   111  	if err != nil {
   112  		return nil, http.StatusForbidden, fmt.Errorf("Error getting CodeArtifact asset: %v: %w", codeArtGetAssetString(input), err)
   113  	}
   114  	logf("Got CodeArtifact asset: %v", codeArtGetAssetString(input))
   115  
   116  	return output.Asset, 0, nil
   117  }
   118  
   119  func (r *CodeArtifactRepoConfig) Put(ctx context.Context, modPath, version string, goModData, infoData, zipData []byte) error {
   120  
   121  	client, err := r.getClient(ctx)
   122  	if err != nil {
   123  		return fmt.Errorf("%w", err)
   124  	}
   125  
   126  	pkg := codeArtPackageEscape(modPath)
   127  	namespace := codeArtNamespaceDefault(r.Namespace)
   128  
   129  	// Publish info file
   130  	input := &codeartifact.PublishPackageVersionInput{
   131  		AssetName:      aws.String(version + ".info"),
   132  		AssetSHA256:    aws.String(codeArtAssetSHA256(infoData)),
   133  		AssetContent:   bytes.NewReader(infoData),
   134  		Package:        aws.String(pkg),
   135  		PackageVersion: aws.String(version),
   136  		Domain:         r.Domain,
   137  		Namespace:      namespace,
   138  		Repository:     r.Repository,
   139  		DomainOwner:    r.DomainOwner,
   140  		Format:         codeartifactTypes.PackageFormatGeneric,
   141  		Unfinished:     aws.Bool(true),
   142  	}
   143  
   144  	_, err = client.PublishPackageVersion(ctx, input)
   145  	if err != nil {
   146  		return fmt.Errorf("Error publishing CodeArtifact asset: %v: %w", codeArtPublishAssetString(input), err)
   147  	}
   148  	logf("Published CodeArtifact asset: %v", codeArtPublishAssetString(input))
   149  
   150  	// Publish mod file
   151  	input = &codeartifact.PublishPackageVersionInput{
   152  		AssetName:      aws.String(version + ".mod"),
   153  		AssetSHA256:    aws.String(codeArtAssetSHA256(goModData)),
   154  		AssetContent:   bytes.NewReader(goModData),
   155  		Package:        aws.String(pkg),
   156  		PackageVersion: aws.String(version),
   157  		Domain:         r.Domain,
   158  		Namespace:      namespace,
   159  		Repository:     r.Repository,
   160  		DomainOwner:    r.DomainOwner,
   161  		Format:         codeartifactTypes.PackageFormatGeneric,
   162  		Unfinished:     aws.Bool(true),
   163  	}
   164  
   165  	_, err = client.PublishPackageVersion(ctx, input)
   166  	if err != nil {
   167  		return fmt.Errorf("Error publishing CodeArtifact asset: %v: %w", codeArtPublishAssetString(input), err)
   168  	}
   169  	logf("Published CodeArtifact asset: %v", codeArtPublishAssetString(input))
   170  
   171  	// Publish zip file
   172  	input = &codeartifact.PublishPackageVersionInput{
   173  		AssetName:      aws.String(version + ".zip"),
   174  		AssetSHA256:    aws.String(codeArtAssetSHA256(zipData)),
   175  		AssetContent:   bytes.NewReader(zipData),
   176  		Package:        aws.String(pkg),
   177  		PackageVersion: aws.String(version),
   178  		Domain:         r.Domain,
   179  		Namespace:      namespace,
   180  		Repository:     r.Repository,
   181  		DomainOwner:    r.DomainOwner,
   182  		Format:         codeartifactTypes.PackageFormatGeneric,
   183  		Unfinished:     aws.Bool(false),
   184  	}
   185  
   186  	_, err = client.PublishPackageVersion(ctx, input)
   187  	if err != nil {
   188  		return fmt.Errorf("Error publishing CodeArtifact asset: %v: %w", codeArtPublishAssetString(input), err)
   189  	}
   190  	logf("Published CodeArtifact asset: %v", codeArtPublishAssetString(input))
   191  
   192  	return nil
   193  }
   194  
   195  func (r *CodeArtifactRepoConfig) getClient(ctx context.Context) (CodeArtifactClient, error) {
   196  	if r.client == nil {
   197  		config, err := awsconfig.LoadDefaultConfig(ctx)
   198  		if err != nil {
   199  			return nil, fmt.Errorf("Error loading AWS config: %w", err)
   200  		}
   201  		r.client = codeartifact.NewFromConfig(config)
   202  	}
   203  	return r.client, nil
   204  }
   205  
   206  func codeArtPackageEscape(pkg string) string {
   207  	// Code Artifact package names must
   208  	// match the following regular expression:
   209  	//   ([a-zA-Z0-9])+([-_+.]?[a-zA-Z0-9])*
   210  	// Go Module paths should be restricted to [-._~/]
   211  	//   https://go.dev/ref/mod#go-mod-file-ident
   212  	// Escape unsupported characters using "plus" encoding
   213  	pkg = strings.ReplaceAll(pkg, "+", "+2B")
   214  	pkg = strings.ReplaceAll(pkg, "/", "+2F")
   215  	pkg = strings.ReplaceAll(pkg, "~", "+7E")
   216  	return pkg
   217  }
   218  
   219  func codeArtNamespaceDefault(namespace *string) *string {
   220  	if namespace == nil {
   221  		return aws.String("goxm")
   222  	}
   223  	return namespace
   224  }
   225  
   226  func codeArtAssetSHA256(data []byte) string {
   227  	return fmt.Sprintf("%x", sha256.Sum256(data))
   228  }
   229  
   230  func codeArtListVersionsString(input *codeartifact.ListPackageVersionsInput) string {
   231  	return fmt.Sprintf(
   232  		"Domain:%v(%v) Repo:%v NS:%v Pkg:%v",
   233  		aws.ToString(input.Domain), aws.ToString(input.DomainOwner),
   234  		aws.ToString(input.Repository), aws.ToString(input.Namespace),
   235  		aws.ToString(input.Package),
   236  	)
   237  }
   238  
   239  func codeArtGetAssetString(input *codeartifact.GetPackageVersionAssetInput) string {
   240  	return fmt.Sprintf(
   241  		"Domain:%v(%v) Repo:%v NS:%v Pkg:%v Version:%v: Asset:%v",
   242  		aws.ToString(input.Domain), aws.ToString(input.DomainOwner),
   243  		aws.ToString(input.Repository), aws.ToString(input.Namespace),
   244  		aws.ToString(input.Package), aws.ToString(input.PackageVersion),
   245  		aws.ToString(input.Asset),
   246  	)
   247  }
   248  
   249  func codeArtPublishAssetString(input *codeartifact.PublishPackageVersionInput) string {
   250  	return fmt.Sprintf(
   251  		"Domain:%v(%v) Repo:%v NS:%v Pkg:%v Version:%v Asset:%v",
   252  		aws.ToString(input.Domain), aws.ToString(input.DomainOwner),
   253  		aws.ToString(input.Repository), aws.ToString(input.Namespace),
   254  		aws.ToString(input.Package), aws.ToString(input.PackageVersion),
   255  		aws.ToString(input.AssetName),
   256  	)
   257  }