2020-05-09 11:45:13 +00:00
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
2020-05-09 13:26:54 +00:00
"sort"
2020-05-09 11:45:13 +00:00
"strings"
log "github.com/golang/glog"
"github.com/google/go-querystring/query"
)
var (
gitlabAccessToken = flag . String ( "gitlab_access_token" , os . Getenv ( "GITLAB_ACCESS_TOKEN" ) , "GitLab API Access Token" )
gitlabEndpoint = flag . String ( "gitlab_endpoint" , "https://hg.lukegb.com" , "GitLab API endpoint" )
gitlabProject = flag . String ( "gitlab_project" , "lukegb/depot" , "Project to sync secrets into" )
2020-05-09 13:26:54 +00:00
manifestVariable = flag . String ( "manifest_variable" , "" , "If non-empty, a variable to populate with the path to a [VARIABLE]=[FILENAME] manifest, separated by newlines." )
2020-05-09 11:45:13 +00:00
varToFileMap map [ string ] string
)
func init ( ) {
flag . Var ( stringMapVar { & varToFileMap } , "variable_to_file" , "[VARIABLE]=[FILENAME] mapping" )
}
type stringMapVar struct {
dst * map [ string ] string
}
func ( v stringMapVar ) String ( ) string {
if v . dst == nil || * v . dst == nil {
return "nil"
}
return fmt . Sprintf ( "%#v" , * v . dst )
}
func ( v stringMapVar ) Set ( s string ) error {
if * v . dst == nil {
* v . dst = make ( map [ string ] string )
}
bits := strings . SplitN ( s , "=" , 2 )
if len ( bits ) != 2 {
return fmt . Errorf ( "not a KEY=VALUE formatted: %v" , s )
}
( * v . dst ) [ bits [ 0 ] ] = bits [ 1 ]
return nil
}
// GitLabAPI encapsulates access to the GitLab API.
type GitLabAPI struct {
Client * http . Client
AccessToken string
Endpoint string
}
func ( gl * GitLabAPI ) url ( subpath string ) ( string , error ) {
u , err := url . Parse ( gl . Endpoint )
if err != nil {
return "" , fmt . Errorf ( "parsing endpoint %q: %w" , gl . Endpoint , err )
}
pv , err := url . PathUnescape ( subpath )
if err != nil {
return "" , fmt . Errorf ( "unescaping %q: %w" , subpath , err )
}
u . Path = path . Join ( u . Path , pv )
u . RawPath = path . Join ( u . RawPath , subpath )
return u . String ( ) , nil
}
func ( gl * GitLabAPI ) do ( ctx context . Context , req * http . Request ) ( * http . Response , error ) {
req = req . WithContext ( ctx )
if gl . AccessToken != "" {
req . Header . Set ( "Authorization" , "Bearer " + gl . AccessToken )
}
resp , err := gl . Client . Do ( req )
if err != nil {
return nil , err
}
if resp . StatusCode >= http . StatusBadRequest {
resp . Body . Close ( )
return nil , fmt . Errorf ( "%v %v failed (status %v)" , req . Method , req . URL , resp . Status )
}
return resp , nil
}
func ( gl * GitLabAPI ) get ( ctx context . Context , path string ) ( * http . Response , error ) {
u , err := gl . url ( path )
if err != nil {
return nil , err
}
req , err := http . NewRequest ( "GET" , u , nil )
if err != nil {
return nil , fmt . Errorf ( "building GET request for %q: %w" , u , err )
}
return gl . do ( ctx , req )
}
func ( gl * GitLabAPI ) doWithForm ( ctx context . Context , method , path string , data url . Values ) ( * http . Response , error ) {
u , err := gl . url ( path )
if err != nil {
return nil , err
}
req , err := http . NewRequest ( method , u , strings . NewReader ( data . Encode ( ) ) )
if err != nil {
return nil , fmt . Errorf ( "building %v request for %q: %w" , method , u , err )
}
req . Header . Set ( "Content-Type" , "application/x-www-form-urlencoded" )
return gl . do ( ctx , req )
}
// Ping just tests to see if the connection is successful by retrieving the projects endpoint.
// The response data is ignored, but the status code is checked.
func ( gl * GitLabAPI ) Ping ( ctx context . Context ) error {
resp , err := gl . get ( ctx , "/api/v4/projects" )
if err != nil {
return err
}
resp . Body . Close ( )
return nil
}
type ProjectVariable struct {
Key string ` json:"key" url:"key" `
VariableType string ` json:"variable_type" url:"variable_type" `
Value string ` json:"value" url:"value" `
Protected bool ` json:"protected" url:"protected" `
Masked bool ` json:"masked" url:"masked" `
}
2020-05-10 11:11:26 +00:00
func ( v ProjectVariable ) EncodedValue ( ) string {
return strings . ReplaceAll ( v . Value , "$" , "$$" )
}
func ( v ProjectVariable ) Values ( ) url . Values {
data , err := query . Values ( v )
if err != nil {
panic ( "query.Values failed unexpectedly" )
}
data . Set ( "value" , v . EncodedValue ( ) )
return data
}
2020-05-09 11:45:13 +00:00
// Variables gets the list of variables from GitLab.
func ( gl * GitLabAPI ) Variables ( ctx context . Context , project string ) ( map [ string ] ProjectVariable , error ) {
resp , err := gl . get ( ctx , fmt . Sprintf ( "/api/v4/projects/%s/variables" , url . PathEscape ( project ) ) )
if err != nil {
return nil , err
}
defer resp . Body . Close ( )
var pvs [ ] ProjectVariable
if err := json . NewDecoder ( resp . Body ) . Decode ( & pvs ) ; err != nil {
return nil , fmt . Errorf ( "decoding GitLab API response: %w" , err )
}
out := make ( map [ string ] ProjectVariable , len ( pvs ) )
for _ , s := range pvs {
2020-05-10 11:11:26 +00:00
s . Value = strings . ReplaceAll ( s . Value , "$$" , "$" )
2020-05-09 11:45:13 +00:00
out [ s . Key ] = s
}
return out , nil
}
// CreateVariable creates a new project variable.
func ( gl * GitLabAPI ) CreateVariable ( ctx context . Context , project string , varData ProjectVariable ) error {
2020-05-10 11:11:26 +00:00
resp , err := gl . doWithForm ( ctx , "POST" , fmt . Sprintf ( "/api/v4/projects/%s/variables" , url . PathEscape ( project ) ) , varData . Values ( ) )
2020-05-09 11:45:13 +00:00
if err != nil {
return err
}
if _ , err := ioutil . ReadAll ( resp . Body ) ; err != nil {
return fmt . Errorf ( "reading response body to completion: %w" , err )
}
resp . Body . Close ( )
return nil
}
// UpdateVariable updates a project variable.
func ( gl * GitLabAPI ) UpdateVariable ( ctx context . Context , project string , varData ProjectVariable ) error {
2020-05-10 11:11:26 +00:00
resp , err := gl . doWithForm ( ctx , "PUT" , fmt . Sprintf ( "/api/v4/projects/%s/variables/%s" , url . PathEscape ( project ) , url . PathEscape ( varData . Key ) ) , varData . Values ( ) )
2020-05-09 11:45:13 +00:00
if err != nil {
return err
}
if _ , err := ioutil . ReadAll ( resp . Body ) ; err != nil {
return fmt . Errorf ( "reading response body to completion: %w" , err )
}
resp . Body . Close ( )
return nil
}
func main ( ) {
flag . Parse ( )
defer log . Flush ( )
ctx := context . Background ( )
gitlab := & GitLabAPI {
Client : http . DefaultClient ,
AccessToken : * gitlabAccessToken ,
Endpoint : * gitlabEndpoint ,
}
if err := gitlab . Ping ( ctx ) ; err != nil {
log . Exitf ( "gitlab.Ping: %v" , err )
}
vars , err := gitlab . Variables ( ctx , * gitlabProject )
if err != nil {
log . Exitf ( "gitlab.Variables: %v" , err )
}
2020-05-09 13:26:54 +00:00
createOrReplaceVariable := func ( varFuture ProjectVariable ) error {
varCurrent , ok := vars [ varFuture . Key ]
switch {
case ! ok :
log . Infof ( "%q needs to be created" , varFuture . Key )
return gitlab . CreateVariable ( ctx , * gitlabProject , varFuture )
case varCurrent . Value == varFuture . Value :
log . Infof ( "%q up-to-date" , varFuture . Key )
return nil
default :
log . Infof ( "%q needs to be updated" , varFuture . Key )
return gitlab . UpdateVariable ( ctx , * gitlabProject , varFuture )
}
}
2020-05-09 11:45:13 +00:00
for varKey , varSource := range varToFileMap {
2020-05-09 13:26:54 +00:00
varFutureData , err := ioutil . ReadFile ( varSource )
2020-05-09 11:45:13 +00:00
if err != nil {
log . Exitf ( "reading secret source file %q: %v" , varSource , err )
}
2020-05-09 13:26:54 +00:00
if err := createOrReplaceVariable ( ProjectVariable {
2020-05-09 11:45:13 +00:00
Key : varKey ,
VariableType : "file" ,
2020-05-09 13:26:54 +00:00
Value : string ( varFutureData ) ,
2020-05-09 11:45:13 +00:00
Protected : true ,
2020-05-09 13:26:54 +00:00
} ) ; err != nil {
log . Errorf ( "createOrReplaceVariable(%q, %q): %v" , * gitlabProject , varKey , err )
2020-05-09 11:45:13 +00:00
}
2020-05-09 13:26:54 +00:00
}
if * manifestVariable != "" {
var manifestData [ ] string
for k , v := range varToFileMap {
manifestData = append ( manifestData , fmt . Sprintf ( "%s=%s" , k , v ) )
}
sort . Strings ( manifestData )
if err := createOrReplaceVariable ( ProjectVariable {
Key : * manifestVariable ,
VariableType : "file" ,
Value : strings . Join ( manifestData , "\n" ) + "\n" ,
Protected : true ,
} ) ; err != nil {
log . Errorf ( "createOrReplaceVariable(%q, %q): %v" , * gitlabProject , * manifestVariable , err )
2020-05-09 11:45:13 +00:00
}
}
}