mirror of
https://github.com/open-gitagent/langship.sh.git
synced 2026-08-03 07:21:04 +02:00
feat: Implement AI Gateway page with feature tiles and descriptions
feat: Update AppSidebar to include new Environments and Credentials sections feat: Enhance node form with branch selection for triggers and deployment feat: Create CredentialForm and CredentialRow components for managing credentials feat: Add API endpoints for credential management fix: Update node catalog defaults for deploy and promote nodes
This commit is contained in:
@@ -65,6 +65,13 @@ func (m *Mongo) Runs() RunStore { return &mongoRuns{coll: m.db.Collection("runs"
|
||||
// Agents returns the AgentStore backed by this Mongo connection.
|
||||
func (m *Mongo) Agents() AgentStore { return &mongoAgents{coll: m.db.Collection("agents")} }
|
||||
|
||||
// Credentials returns the global CredentialStore backed by this Mongo
|
||||
// connection. Per-agent overrides live on agent.Credentials and are not
|
||||
// persisted here; this collection is the org-wide pool.
|
||||
func (m *Mongo) Credentials() CredentialStore {
|
||||
return &mongoCredentials{coll: m.db.Collection("credentials")}
|
||||
}
|
||||
|
||||
func (m *Mongo) ensureIndexes(ctx context.Context) error {
|
||||
if _, err := m.db.Collection("pipelines").Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
{Keys: bson.D{{Key: "updated_at", Value: -1}}},
|
||||
@@ -82,6 +89,12 @@ func (m *Mongo) ensureIndexes(ctx context.Context) error {
|
||||
}); err != nil {
|
||||
return fmt.Errorf("agents indexes: %w", err)
|
||||
}
|
||||
if _, err := m.db.Collection("credentials").Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
{Keys: bson.D{{Key: "name", Value: 1}}, Options: options.Index().SetUnique(true)},
|
||||
{Keys: bson.D{{Key: "updated_at", Value: -1}}},
|
||||
}); err != nil {
|
||||
return fmt.Errorf("credentials indexes: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -447,3 +460,65 @@ func (s *mongoAgents) List(ctx context.Context) ([]*Agent, error) {
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// --- credentials (global pool) -------------------------------------------
|
||||
|
||||
type mongoCredentials struct{ coll *mongo.Collection }
|
||||
|
||||
func (s *mongoCredentials) Create(ctx context.Context, c *Credential) error {
|
||||
if _, err := s.coll.InsertOne(ctx, c); err != nil {
|
||||
// Surface the duplicate-name index violation as a typed error so
|
||||
// the API layer can return 409 instead of a generic 500.
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
return ErrAlreadyExists
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mongoCredentials) GetByName(ctx context.Context, name string) (*Credential, error) {
|
||||
var c Credential
|
||||
if err := s.coll.FindOne(ctx, bson.M{"name": name}).Decode(&c); err != nil {
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (s *mongoCredentials) Update(ctx context.Context, c *Credential) error {
|
||||
res, err := s.coll.ReplaceOne(ctx, bson.M{"name": c.Name}, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.MatchedCount == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mongoCredentials) Delete(ctx context.Context, name string) error {
|
||||
res, err := s.coll.DeleteOne(ctx, bson.M{"name": name})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.DeletedCount == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mongoCredentials) List(ctx context.Context) ([]*Credential, error) {
|
||||
cur, err := s.coll.Find(ctx, bson.M{}, options.Find().SetSort(bson.D{{Key: "updated_at", Value: -1}}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cur.Close(ctx)
|
||||
var out []*Credential
|
||||
if err := cur.All(ctx, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
+82
-2
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -14,6 +15,11 @@ import (
|
||||
// handlers should map this to HTTP 404.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// ErrAlreadyExists is returned by Create when a uniqueness constraint
|
||||
// (e.g. credential name) would be violated. API handlers should map
|
||||
// this to HTTP 409.
|
||||
var ErrAlreadyExists = errors.New("already exists")
|
||||
|
||||
// Pipeline is the persisted shape of a pipeline (formerly "flow") that the
|
||||
// API stores and returns to the UI. Definition is the n8n-format JSON.
|
||||
type Pipeline struct {
|
||||
@@ -69,6 +75,49 @@ const (
|
||||
AuthFailed AuthStatus = "failed"
|
||||
)
|
||||
|
||||
// CredentialType discriminates the shape of a credential record. Only
|
||||
// fields belonging to the matching type should be populated; the rest
|
||||
// stay zero-valued.
|
||||
type CredentialType string
|
||||
|
||||
const (
|
||||
CredentialAWS CredentialType = "aws"
|
||||
CredentialGCP CredentialType = "gcp"
|
||||
CredentialKV CredentialType = "kv"
|
||||
)
|
||||
|
||||
// Credential is a named credential record attached to an agent. The
|
||||
// Deploy node (and any other node that needs cloud creds) looks one up
|
||||
// by Name. Secret fields are encrypted at rest with pkg/secrets; the API
|
||||
// layer never returns them — only `HasSecret` flags.
|
||||
//
|
||||
// Fields are deliberately flat instead of `union { aws, gcp, kv }` so
|
||||
// the Mongo schema stays simple and an upgrade to a new type only adds
|
||||
// fields without rewriting the doc shape.
|
||||
type Credential struct {
|
||||
ID string `json:"id" bson:"id"`
|
||||
Name string `json:"name" bson:"name"` // unique within an agent
|
||||
Type CredentialType `json:"type" bson:"type"`
|
||||
|
||||
// AWS — non-secret. The Flow host's own identity AssumeRoles into
|
||||
// AwsCrossAccountRoleArn at deploy time.
|
||||
AwsRegion string `json:"awsRegion,omitempty" bson:"aws_region,omitempty"`
|
||||
AwsAccountID string `json:"awsAccountId,omitempty" bson:"aws_account_id,omitempty"`
|
||||
AwsCrossAccountRoleArn string `json:"awsCrossAccountRoleArn,omitempty" bson:"aws_cross_account_role_arn,omitempty"`
|
||||
|
||||
// GCP — projectId / location are non-secret. Service account JSON is.
|
||||
GcpProjectID string `json:"gcpProjectId,omitempty" bson:"gcp_project_id,omitempty"`
|
||||
GcpLocation string `json:"gcpLocation,omitempty" bson:"gcp_location,omitempty"`
|
||||
GcpServiceAccountSealed string `json:"-" bson:"gcp_sa_sealed,omitempty"`
|
||||
|
||||
// Generic key-value store. Each value is sealed independently so we
|
||||
// can return a list of keys publicly without leaking values.
|
||||
KvSealed map[string]string `json:"-" bson:"kv_sealed,omitempty"`
|
||||
|
||||
CreatedAt time.Time `json:"createdAt" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// Agent is an agent repo registered with Langship. The PAT and webhook
|
||||
// secret are stored server-side; the API layer scrubs them before the
|
||||
// record leaves the boundary (see pkg/api/agents.go).
|
||||
@@ -84,8 +133,26 @@ type Agent struct {
|
||||
AuthStatus AuthStatus `json:"authStatus,omitempty" bson:"auth_status,omitempty"`
|
||||
AuthCheckedAt *time.Time `json:"authCheckedAt,omitempty" bson:"auth_checked_at,omitempty"`
|
||||
AttachedPipelines []string `json:"attachedPipelines,omitempty" bson:"attached_pipelines,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updated_at"`
|
||||
|
||||
// Named credentials — referenced by name from Deploy / future nodes.
|
||||
Credentials []Credential `json:"credentials,omitempty" bson:"credentials,omitempty"`
|
||||
|
||||
CreatedAt time.Time `json:"createdAt" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" bson:"updated_at"`
|
||||
}
|
||||
|
||||
// LookupCredential returns the agent's credential matching name (case-
|
||||
// insensitive) and an ok flag. Convenience for executors.
|
||||
func (a *Agent) LookupCredential(name string) (Credential, bool) {
|
||||
if a == nil {
|
||||
return Credential{}, false
|
||||
}
|
||||
for _, c := range a.Credentials {
|
||||
if strings.EqualFold(c.Name, name) {
|
||||
return c, true
|
||||
}
|
||||
}
|
||||
return Credential{}, false
|
||||
}
|
||||
|
||||
// AgentStore persists agent registrations. Update mutates the entire
|
||||
@@ -97,3 +164,16 @@ type AgentStore interface {
|
||||
Delete(ctx context.Context, id string) error
|
||||
List(ctx context.Context) ([]*Agent, error)
|
||||
}
|
||||
|
||||
// CredentialStore persists global (org-wide) credentials. Agents can
|
||||
// override these by name with a record on agent.Credentials, but the
|
||||
// global pool is the canonical place to define a credential once and
|
||||
// reuse it across many agents/pipelines. Lookup is by name (the user-
|
||||
// facing identifier — Deploy nodes reference creds by name, not ID).
|
||||
type CredentialStore interface {
|
||||
Create(ctx context.Context, c *Credential) error
|
||||
GetByName(ctx context.Context, name string) (*Credential, error)
|
||||
Update(ctx context.Context, c *Credential) error
|
||||
Delete(ctx context.Context, name string) error
|
||||
List(ctx context.Context) ([]*Credential, error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user