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 }