github.com/zntrio/harp/v2@v2.0.9/pkg/tasks/to/gha.go (about)

     1  // Licensed to Elasticsearch B.V. under one or more contributor
     2  // license agreements. See the NOTICE file distributed with
     3  // this work for additional information regarding copyright
     4  // ownership. Elasticsearch B.V. licenses this file to you under
     5  // the Apache License, Version 2.0 (the "License"); you may
     6  // not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package to
    19  
    20  import (
    21  	"context"
    22  	"crypto/rand"
    23  	"encoding/base64"
    24  	"errors"
    25  	"fmt"
    26  	"os"
    27  
    28  	"github.com/google/go-github/v42/github"
    29  
    30  	"github.com/zntrio/harp/v2/pkg/bundle"
    31  	"github.com/zntrio/harp/v2/pkg/tasks"
    32  
    33  	"golang.org/x/crypto/nacl/box"
    34  	"golang.org/x/oauth2"
    35  )
    36  
    37  type GithubActionTask struct {
    38  	_               struct{}
    39  	ContainerReader tasks.ReaderProvider
    40  	Owner           string
    41  	Repository      string
    42  	SecretFilter    string
    43  }
    44  
    45  func (t *GithubActionTask) Run(ctx context.Context) error {
    46  	// Create the reader
    47  	reader, err := t.ContainerReader(ctx)
    48  	if err != nil {
    49  		return fmt.Errorf("unable to open input bundle reader: %w", err)
    50  	}
    51  
    52  	// Extract bundle from container
    53  	b, err := bundle.FromContainerReader(reader)
    54  	if err != nil {
    55  		return fmt.Errorf("unable to load bundle: %w", err)
    56  	}
    57  
    58  	// Prepae github API client
    59  	client, err := t.prepareClient(ctx)
    60  	if err != nil {
    61  		return fmt.Errorf("unable to prepare github api client: %w", err)
    62  	}
    63  
    64  	// Retrieve repository public key
    65  	keyID, boxKey, err := t.getRepositoryKey(ctx, client)
    66  	if err != nil {
    67  		return fmt.Errorf("unable to retieve repository public key: %w", err)
    68  	}
    69  
    70  	// Requests to send to github
    71  	githubSecrets := []*github.EncryptedSecret{}
    72  
    73  	// Iterate over packages
    74  	for _, p := range b.Packages {
    75  		// Ignore nil secret chain
    76  		if p.Secrets == nil {
    77  			continue
    78  		}
    79  
    80  		// Get secrets
    81  		secretMap, err := bundle.AsSecretMap(p)
    82  		if err != nil {
    83  			return fmt.Errorf("unable to retrieve secrets from %q package: %w", p.Name, err)
    84  		}
    85  
    86  		// Filter secrets map using given filter glob.
    87  		filteredSecrets := secretMap.Glob(t.SecretFilter)
    88  
    89  		// Iterate over secrets
    90  		for secretKey, value := range filteredSecrets {
    91  			var secretBytes []byte
    92  
    93  			// Pack the secret value
    94  			switch v := value.(type) {
    95  			case string:
    96  				secretBytes = []byte(v)
    97  			case []byte:
    98  				secretBytes = v
    99  			default:
   100  				return fmt.Errorf("can't process secret type %T", value)
   101  			}
   102  
   103  			// The secret is encrypted with box.SealAnonymous using the repo's decoded public key.
   104  			encryptedBytes, err := box.SealAnonymous([]byte{}, secretBytes, boxKey, rand.Reader)
   105  			if err != nil {
   106  				return fmt.Errorf("unable to encrypt the secret payload: %w", err)
   107  			}
   108  
   109  			// Prepare the request
   110  			githubSecrets = append(githubSecrets, &github.EncryptedSecret{
   111  				Name:           secretKey,
   112  				KeyID:          keyID,
   113  				EncryptedValue: base64.StdEncoding.EncodeToString(encryptedBytes),
   114  			})
   115  		}
   116  	}
   117  
   118  	// Publish all secrets
   119  	for _, gs := range githubSecrets {
   120  		// Create or update the secret value.
   121  		if _, err := client.Actions.CreateOrUpdateRepoSecret(ctx, t.Owner, t.Repository, gs); err != nil {
   122  			return fmt.Errorf("unable to publish secret to github: %w", err)
   123  		}
   124  	}
   125  
   126  	// No error
   127  	return nil
   128  }
   129  
   130  func (t *GithubActionTask) prepareClient(ctx context.Context) (*github.Client, error) {
   131  	// Retrieve github token
   132  	githubToken := os.Getenv("GITHUB_TOKEN")
   133  	if githubToken == "" {
   134  		return nil, errors.New("GITHUB_TOKEN environment variable must be set")
   135  	}
   136  
   137  	// Create an authenticated transport
   138  	tc := oauth2.NewClient(
   139  		ctx,
   140  		oauth2.StaticTokenSource(
   141  			&oauth2.Token{
   142  				AccessToken: githubToken,
   143  			},
   144  		),
   145  	)
   146  
   147  	// Create github API client
   148  	client := github.NewClient(tc)
   149  
   150  	// No error
   151  	return client, nil
   152  }
   153  
   154  func (t *GithubActionTask) getRepositoryKey(ctx context.Context, client *github.Client) (keyID string, publicKey *[32]byte, err error) {
   155  	// Retrieve repository public key
   156  	pub, _, err := client.Actions.GetRepoPublicKey(ctx, t.Owner, t.Repository)
   157  	if err != nil {
   158  		return "", nil, fmt.Errorf("unable to retrieve repository public key for secret encryption: %w", err)
   159  	}
   160  
   161  	// Decode public key.
   162  	decodedPublicKey, err := base64.StdEncoding.DecodeString(pub.GetKey())
   163  	if err != nil {
   164  		return pub.GetKeyID(), nil, fmt.Errorf("unable to decode public key from github: %w", err)
   165  	}
   166  
   167  	// The decode key is converted into a fixed size byte array.
   168  	var boxKey [32]byte
   169  
   170  	// The secret value is converted into a slice of bytes.
   171  	copy(boxKey[:], decodedPublicKey)
   172  
   173  	// No error
   174  	return pub.GetKeyID(), &boxKey, nil
   175  }