~sircmpwn/builds.sr.ht

9c13d60a0d002937d03b9baef1301859ef0c5cb7 — Drew DeVault 22 days ago fdccf0e
API: rig up job { secrets }
M api/go.mod => api/go.mod +1 -0
@@ 8,4 8,5 @@ require (
	github.com/Masterminds/squirrel v1.4.0 // indirect
	github.com/lib/pq v1.8.0 // indirect
	github.com/vektah/gqlparser/v2 v2.1.0
	gopkg.in/yaml.v2 v2.3.0 // indirect
)

M api/graph/api/generated.go => api/graph/api/generated.go +102 -35
@@ 41,7 41,10 @@ type ResolverRoot interface {
	Job() JobResolver
	JobGroup() JobGroupResolver
	Mutation() MutationResolver
	PGPKey() PGPKeyResolver
	Query() QueryResolver
	SSHKey() SSHKeyResolver
	SecretFile() SecretFileResolver
	Task() TaskResolver
	User() UserResolver
}


@@ 220,6 223,9 @@ type MutationResolver interface {
	UpdateTask(ctx context.Context, taskID int, status model.TaskStatus) (*model.Job, error)
	CreateArtifact(ctx context.Context, jobID int, path string, contents string) (*model.Artifact, error)
}
type PGPKeyResolver interface {
	PrivateKey(ctx context.Context, obj *model.PGPKey) (string, error)
}
type QueryResolver interface {
	Version(ctx context.Context) (*model.Version, error)
	Me(ctx context.Context) (*model.User, error)


@@ 229,6 235,12 @@ type QueryResolver interface {
	Job(ctx context.Context, id int) (*model.Job, error)
	Secrets(ctx context.Context, cursor *model1.Cursor) (*model.SecretCursor, error)
}
type SSHKeyResolver interface {
	PrivateKey(ctx context.Context, obj *model.SSHKey) (string, error)
}
type SecretFileResolver interface {
	Data(ctx context.Context, obj *model.SecretFile) (string, error)
}
type TaskResolver interface {
	Log(ctx context.Context, obj *model.Task) (*model.Log, error)
	Job(ctx context.Context, obj *model.Task) (*model.Job, error)


@@ 1148,7 1160,7 @@ type Job {
  log: Log @access(scope: LOGS, kind: RO)

  # List of secrets available to this job, or null if they were disabled
  secrets: [Secret]
  secrets: [Secret] @access(scope: SECRETS, kind: RO)
}

type Log {


@@ 2650,8 2662,36 @@ func (ec *executionContext) _Job_secrets(ctx context.Context, field graphql.Coll

	ctx = graphql.WithFieldContext(ctx, fc)
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return ec.resolvers.Job().Secrets(rctx, obj)
		directive0 := func(rctx context.Context) (interface{}, error) {
			ctx = rctx // use context from middleware stack in children
			return ec.resolvers.Job().Secrets(rctx, obj)
		}
		directive1 := func(ctx context.Context) (interface{}, error) {
			scope, err := ec.unmarshalNAccessScope2gitᚗsrᚗhtᚋאsircmpwnᚋbuildsᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "SECRETS")
			if err != nil {
				return nil, err
			}
			kind, err := ec.unmarshalNAccessKind2gitᚗsrᚗhtᚋאsircmpwnᚋbuildsᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐAccessKind(ctx, "RO")
			if err != nil {
				return nil, err
			}
			if ec.directives.Access == nil {
				return nil, errors.New("directive access is not implemented")
			}
			return ec.directives.Access(ctx, obj, directive0, scope, kind)
		}

		tmp, err := directive1(rctx)
		if err != nil {
			return nil, graphql.ErrorOnPath(ctx, err)
		}
		if tmp == nil {
			return nil, nil
		}
		if data, ok := tmp.([]model.Secret); ok {
			return data, nil
		}
		return nil, fmt.Errorf(`unexpected type %T from directive, should be []git.sr.ht/~sircmpwn/builds.sr.ht/api/graph/model.Secret`, tmp)
	})
	if err != nil {
		ec.Error(ctx, err)


@@ 3829,15 3869,15 @@ func (ec *executionContext) _PGPKey_privateKey(ctx context.Context, field graphq
		Object:     "PGPKey",
		Field:      field,
		Args:       nil,
		IsMethod:   false,
		IsResolver: false,
		IsMethod:   true,
		IsResolver: true,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		directive0 := func(rctx context.Context) (interface{}, error) {
			ctx = rctx // use context from middleware stack in children
			return obj.PrivateKey, nil
			return ec.resolvers.PGPKey().PrivateKey(rctx, obj)
		}
		directive1 := func(ctx context.Context) (interface{}, error) {
			if ec.directives.Worker == nil {


@@ 4531,15 4571,15 @@ func (ec *executionContext) _SSHKey_privateKey(ctx context.Context, field graphq
		Object:     "SSHKey",
		Field:      field,
		Args:       nil,
		IsMethod:   false,
		IsResolver: false,
		IsMethod:   true,
		IsResolver: true,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		directive0 := func(rctx context.Context) (interface{}, error) {
			ctx = rctx // use context from middleware stack in children
			return obj.PrivateKey, nil
			return ec.resolvers.SSHKey().PrivateKey(rctx, obj)
		}
		directive1 := func(ctx context.Context) (interface{}, error) {
			if ec.directives.Worker == nil {


@@ 4860,15 4900,15 @@ func (ec *executionContext) _SecretFile_data(ctx context.Context, field graphql.
		Object:     "SecretFile",
		Field:      field,
		Args:       nil,
		IsMethod:   false,
		IsResolver: false,
		IsMethod:   true,
		IsResolver: true,
	}

	ctx = graphql.WithFieldContext(ctx, fc)
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		directive0 := func(rctx context.Context) (interface{}, error) {
			ctx = rctx // use context from middleware stack in children
			return obj.Data, nil
			return ec.resolvers.SecretFile().Data(rctx, obj)
		}
		directive1 := func(ctx context.Context) (interface{}, error) {
			if ec.directives.Worker == nil {


@@ 7440,25 7480,34 @@ func (ec *executionContext) _PGPKey(ctx context.Context, sel ast.SelectionSet, o
		case "id":
			out.Values[i] = ec._PGPKey_id(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "created":
			out.Values[i] = ec._PGPKey_created(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "uuid":
			out.Values[i] = ec._PGPKey_uuid(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "name":
			out.Values[i] = ec._PGPKey_name(ctx, field, obj)
		case "privateKey":
			out.Values[i] = ec._PGPKey_privateKey(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
			}
			field := field
			out.Concurrently(i, func() (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._PGPKey_privateKey(ctx, field, obj)
				if res == graphql.Null {
					atomic.AddUint32(&invalids, 1)
				}
				return res
			})
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}


@@ 7603,25 7652,34 @@ func (ec *executionContext) _SSHKey(ctx context.Context, sel ast.SelectionSet, o
		case "id":
			out.Values[i] = ec._SSHKey_id(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "created":
			out.Values[i] = ec._SSHKey_created(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "uuid":
			out.Values[i] = ec._SSHKey_uuid(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "name":
			out.Values[i] = ec._SSHKey_name(ctx, field, obj)
		case "privateKey":
			out.Values[i] = ec._SSHKey_privateKey(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
			}
			field := field
			out.Concurrently(i, func() (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._SSHKey_privateKey(ctx, field, obj)
				if res == graphql.Null {
					atomic.AddUint32(&invalids, 1)
				}
				return res
			})
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}


@@ 7676,35 7734,44 @@ func (ec *executionContext) _SecretFile(ctx context.Context, sel ast.SelectionSe
		case "id":
			out.Values[i] = ec._SecretFile_id(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "created":
			out.Values[i] = ec._SecretFile_created(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "uuid":
			out.Values[i] = ec._SecretFile_uuid(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "name":
			out.Values[i] = ec._SecretFile_name(ctx, field, obj)
		case "path":
			out.Values[i] = ec._SecretFile_path(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "mode":
			out.Values[i] = ec._SecretFile_mode(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
				atomic.AddUint32(&invalids, 1)
			}
		case "data":
			out.Values[i] = ec._SecretFile_data(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				invalids++
			}
			field := field
			out.Concurrently(i, func() (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._SecretFile_data(ctx, field, obj)
				if res == graphql.Null {
					atomic.AddUint32(&invalids, 1)
				}
				return res
			})
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}

M api/graph/model/models_gen.go => api/graph/model/models_gen.go +0 -36
@@ 15,10 15,6 @@ type Entity interface {
	IsEntity()
}

type Secret interface {
	IsSecret()
}

type Trigger interface {
	IsTrigger()
}


@@ 48,43 44,11 @@ type Log struct {
	FullURL    string `json:"fullURL"`
}

type PGPKey struct {
	ID         int       `json:"id"`
	Created    time.Time `json:"created"`
	UUID       string    `json:"uuid"`
	Name       *string   `json:"name"`
	PrivateKey string    `json:"privateKey"`
}

func (PGPKey) IsSecret() {}

type SSHKey struct {
	ID         int       `json:"id"`
	Created    time.Time `json:"created"`
	UUID       string    `json:"uuid"`
	Name       *string   `json:"name"`
	PrivateKey string    `json:"privateKey"`
}

func (SSHKey) IsSecret() {}

type SecretCursor struct {
	Results []Secret      `json:"results"`
	Cursor  *model.Cursor `json:"cursor"`
}

type SecretFile struct {
	ID      int       `json:"id"`
	Created time.Time `json:"created"`
	UUID    string    `json:"uuid"`
	Name    *string   `json:"name"`
	Path    string    `json:"path"`
	Mode    int       `json:"mode"`
	Data    string    `json:"data"`
}

func (SecretFile) IsSecret() {}

type TriggerInput struct {
	Type      TriggerType          `json:"type"`
	Condition TriggerCondition     `json:"condition"`

A api/graph/model/secrets.go => api/graph/model/secrets.go +183 -0
@@ 0,0 1,183 @@
package model

import (
	"context"
	"database/sql"
	"time"
	"strconv"

	sq "github.com/Masterminds/squirrel"

	"git.sr.ht/~sircmpwn/core-go/model"
	"git.sr.ht/~sircmpwn/core-go/database"
)

const (
	SECRET_PGPKEY = "pgp_key"
	SECRET_SSHKEY = "ssh_key"
	SECRET_FILE = "plaintext_file"
)

type Secret interface {
	IsSecret()
}

type RawSecret struct {
	ID         int
	Created    time.Time
	UUID       string
	SecretType string
	Secret     []byte
	Name       *string
	Path       *string
	Mode       *int

	alias  string
	fields *database.ModelFields
}

func (s *RawSecret) As(alias string) *RawSecret {
	s.alias = alias
	return s
}

func (s *RawSecret) Alias() string {
	return s.alias
}

func (s *RawSecret) Table() string {
	return `"secret"`
}

type PGPKey struct {
	ID         int       `json:"id"`
	Created    time.Time `json:"created"`
	UUID       string    `json:"uuid"`
	Name       *string   `json:"name"`
	PrivateKey []byte    `json:"privateKey"`
}

func (PGPKey) IsSecret() {}

type SSHKey struct {
	ID         int       `json:"id"`
	Created    time.Time `json:"created"`
	UUID       string    `json:"uuid"`
	Name       *string   `json:"name"`
	PrivateKey []byte    `json:"privateKey"`
}

func (SSHKey) IsSecret() {}

type SecretFile struct {
	ID      int       `json:"id"`
	Created time.Time `json:"created"`
	UUID    string    `json:"uuid"`
	Name    *string   `json:"name"`
	Path    string    `json:"path"`
	Mode    int       `json:"mode"`
	Data    []byte    `json:"data"`
}

func (SecretFile) IsSecret() {}

func (s *RawSecret) ToSecret() Secret {
	switch s.SecretType {
	case SECRET_PGPKEY:
		return &PGPKey{
			ID: s.ID,
			Created: s.Created,
			UUID: s.UUID,
			Name: s.Name,
			PrivateKey: s.Secret,
		}
	case SECRET_SSHKEY:
		return &SSHKey{
			ID: s.ID,
			Created: s.Created,
			UUID: s.UUID,
			Name: s.Name,
			PrivateKey: s.Secret,
		}
	case SECRET_FILE:
		return &SecretFile{
			ID: s.ID,
			Created: s.Created,
			UUID: s.UUID,
			Name: s.Name,
			Path: *s.Path,
			Mode: *s.Mode,
			Data: s.Secret,
		}
	default:
		panic("Database invariant broken: unknown secret type")
	}
}

func (s *RawSecret) Fields() *database.ModelFields {
	if s.fields != nil {
		return s.fields
	}
	s.fields = &database.ModelFields{
		Fields: []*database.FieldMap{
			{ "created", "created", &s.Created },
			{ "uuid", "uuid", &s.UUID },
			{ "name", "name", &s.Name },

			// Always fetch:
			{ "id", "", &s.ID },
			{ "secret_type", "", &s.SecretType },
			{ "secret", "", &s.Secret },
			{ "path", "", &s.Path },
			{ "mode", "", &s.Mode },
		},
	}
	return s.fields
}

func (s *RawSecret) QueryWithCursor(ctx context.Context, runner sq.BaseRunner,
	q sq.SelectBuilder, cur *model.Cursor) ([]Secret, *model.Cursor) {
	var (
		err  error
		rows *sql.Rows
	)

	if cur.Next != "" {
		next, _ := strconv.ParseInt(cur.Next, 10, 64)
		q = q.Where(database.WithAlias(s.alias, "id")+"<= ?", next)
	}
	q = q.
		OrderBy(database.WithAlias(s.alias, "id") + " DESC").
		Limit(uint64(cur.Count + 1))

	if rows, err = q.RunWith(runner).QueryContext(ctx); err != nil {
		panic(err)
	}
	defer rows.Close()

	var (
		secrets []Secret
		lastId  int
	)
	for rows.Next() {
		var secret RawSecret
		if err := rows.Scan(database.Scan(ctx, &secret)...); err != nil {
			panic(err)
		}
		lastId = secret.ID
		secrets = append(secrets, secret.ToSecret())
	}

	if len(secrets) > cur.Count {
		cur = &model.Cursor{
			Count:  cur.Count,
			Next:   strconv.Itoa(lastId),
			Search: cur.Search,
		}
		secrets = secrets[:cur.Count]
	} else {
		cur = nil
	}

	return secrets, cur
}

M api/graph/schema.graphqls => api/graph/schema.graphqls +1 -1
@@ 99,7 99,7 @@ type Job {
  log: Log @access(scope: LOGS, kind: RO)

  # List of secrets available to this job, or null if they were disabled
  secrets: [Secret]
  secrets: [Secret] @access(scope: SECRETS, kind: RO)
}

type Log {

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +78 -1
@@ 13,7 13,10 @@ import (
	"git.sr.ht/~sircmpwn/builds.sr.ht/api/loaders"
	"git.sr.ht/~sircmpwn/core-go/auth"
	"git.sr.ht/~sircmpwn/core-go/database"
	"github.com/lib/pq"
	coremodel "git.sr.ht/~sircmpwn/core-go/model"
	sq "github.com/Masterminds/squirrel"
	yaml "gopkg.in/yaml.v2"
)

func (r *jobResolver) Owner(ctx context.Context, obj *model.Job) (model.Entity, error) {


@@ 103,7 106,57 @@ func (r *jobResolver) Log(ctx context.Context, obj *model.Job) (*model.Log, erro
}

func (r *jobResolver) Secrets(ctx context.Context, obj *model.Job) ([]model.Secret, error) {
	panic(fmt.Errorf("not implemented"))
	var secrets []model.Secret

	if err := database.WithTx(ctx, &sql.TxOptions{
		Isolation: 0,
		ReadOnly:  true,
	}, func(tx *sql.Tx) error {
		row := tx.QueryRowContext(ctx,
			`SELECT manifest FROM job WHERE id = $1`, obj.ID)

		var rawManifest string
		if err := row.Scan(&rawManifest); err != nil {
			return err
		}

		type Manifest struct {
			Secrets []string `yaml:"secrets"`
		}

		var manifest Manifest
		if err := yaml.Unmarshal([]byte(rawManifest), &manifest); err != nil {
			return err
		}

		secret := (&model.RawSecret{}).As(`sec`)
		rows, err := database.
			Select(ctx, secret).
			From(`secret sec`).
			Where(sq.Expr(`sec.uuid = ANY(?)`, pq.Array(manifest.Secrets))).
			Where(`sec.user_id = ? AND sec.user_id = ?`,
				obj.OwnerID, auth.ForContext(ctx).UserID).
			RunWith(tx).
			QueryContext(ctx)
		if err != nil {
			panic(err)
		}
		defer rows.Close()

		for rows.Next() {
			var sec model.RawSecret
			if err := rows.Scan(database.Scan(ctx, &sec)...); err != nil {
				panic(err)
			}
			secrets = append(secrets, sec.ToSecret())
		}

		return nil
	}); err != nil {
		return nil, err
	}

	return secrets, nil
}

func (r *jobGroupResolver) Owner(ctx context.Context, obj *model.JobGroup) (model.Entity, error) {


@@ 187,6 240,10 @@ func (r *mutationResolver) CreateArtifact(ctx context.Context, jobID int, path s
	panic(fmt.Errorf("not implemented"))
}

func (r *pGPKeyResolver) PrivateKey(ctx context.Context, obj *model.PGPKey) (string, error) {
	panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) {
	return &model.Version{
		Major:           0,


@@ 250,6 307,14 @@ func (r *queryResolver) Secrets(ctx context.Context, cursor *coremodel.Cursor) (
	panic(fmt.Errorf("not implemented"))
}

func (r *sSHKeyResolver) PrivateKey(ctx context.Context, obj *model.SSHKey) (string, error) {
	panic(fmt.Errorf("not implemented"))
}

func (r *secretFileResolver) Data(ctx context.Context, obj *model.SecretFile) (string, error) {
	panic(fmt.Errorf("not implemented"))
}

func (r *taskResolver) Log(ctx context.Context, obj *model.Task) (*model.Log, error) {
	if obj.Runner == nil {
		return nil, nil


@@ 295,9 360,18 @@ func (r *Resolver) JobGroup() api.JobGroupResolver { return &jobGroupResolver{r}
// Mutation returns api.MutationResolver implementation.
func (r *Resolver) Mutation() api.MutationResolver { return &mutationResolver{r} }

// PGPKey returns api.PGPKeyResolver implementation.
func (r *Resolver) PGPKey() api.PGPKeyResolver { return &pGPKeyResolver{r} }

// Query returns api.QueryResolver implementation.
func (r *Resolver) Query() api.QueryResolver { return &queryResolver{r} }

// SSHKey returns api.SSHKeyResolver implementation.
func (r *Resolver) SSHKey() api.SSHKeyResolver { return &sSHKeyResolver{r} }

// SecretFile returns api.SecretFileResolver implementation.
func (r *Resolver) SecretFile() api.SecretFileResolver { return &secretFileResolver{r} }

// Task returns api.TaskResolver implementation.
func (r *Resolver) Task() api.TaskResolver { return &taskResolver{r} }



@@ 307,6 381,9 @@ func (r *Resolver) User() api.UserResolver { return &userResolver{r} }
type jobResolver struct{ *Resolver }
type jobGroupResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type pGPKeyResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type sSHKeyResolver struct{ *Resolver }
type secretFileResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }
type userResolver struct{ *Resolver }