Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build-and-test:
name: Build & Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
cache: true

- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '24' # Node.js 24 (LTS)
cache: 'npm'
cache-dependency-path: 'web/package-lock.json'

- name: Build Web UI
working-directory: ./web
run: |
npm ci
npm run build

- name: Verify Web Build
run: ls -la web/dist

- name: Format Check
run: |
if [ "$(gofmt -l . | wc -l)" -gt 0 ]; then
echo "Files need formatting. Run 'go fmt ./...'"
gofmt -d .
exit 1
fi

- name: Vet Code
run: make lint

- name: Run Tests
run: make test-fast

- name: Build Binary
run: make build

lint:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
cache: true

- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
# Only run on changes in PRs to avoid noise from existing issues
only-new-issues: true
132 changes: 90 additions & 42 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ import (
"github.com/GoogleCloudPlatform/scion/pkg/agent"
"github.com/GoogleCloudPlatform/scion/pkg/agent/state"
"github.com/GoogleCloudPlatform/scion/pkg/api"
"github.com/GoogleCloudPlatform/scion/pkg/broker"
"github.com/GoogleCloudPlatform/scion/pkg/apiclient"
"github.com/GoogleCloudPlatform/scion/pkg/broker"
"github.com/GoogleCloudPlatform/scion/pkg/brokercredentials"
"github.com/GoogleCloudPlatform/scion/pkg/config"
"github.com/GoogleCloudPlatform/scion/pkg/daemon"
Expand Down Expand Up @@ -1843,46 +1843,15 @@ func newAgentDispatcherAdapter(mgr agent.Manager, s store.Store, brokerID string
// DispatchAgentCreate implements hub.AgentDispatcher.
// It starts the agent on the runtime broker and updates the hub store with runtime info.
func (d *agentDispatcherAdapter) DispatchAgentCreate(ctx context.Context, hubAgent *store.Agent) error {
// Look up the local path for this grove on this runtime broker
var grovePath string
if hubAgent.GroveID != "" && d.brokerID != "" {
provider, err := d.store.GetGroveProvider(ctx, hubAgent.GroveID, d.brokerID)
if err != nil {
log.Printf("Warning: failed to get grove provider for path lookup: %v", err)
} else if provider.LocalPath != "" {
grovePath = provider.LocalPath
}
}

// Build StartOptions from the hub agent record
env := make(map[string]string)
if hubAgent.AppliedConfig != nil && hubAgent.AppliedConfig.Env != nil {
env = hubAgent.AppliedConfig.Env
}
grovePath := d.resolveGrovePath(ctx, hubAgent.GroveID)
opts := d.buildStartOptions(hubAgent, grovePath, false)

// Add grove ID label for tracking
// Ensure grove ID label is present for tracking
if hubAgent.Labels == nil {
hubAgent.Labels = make(map[string]string)
}
hubAgent.Labels["scion.grove"] = hubAgent.GroveID

opts := api.StartOptions{
Name: hubAgent.Name,
Template: hubAgent.Template,
Image: hubAgent.Image,
Env: env,
Detached: &hubAgent.Detached,
GrovePath: grovePath, // Pass the local filesystem path for this grove
}

if hubAgent.AppliedConfig != nil {
opts.HarnessConfig = hubAgent.AppliedConfig.HarnessConfig
// Pass the task through to the runtime broker
if hubAgent.AppliedConfig.Task != "" {
opts.Task = hubAgent.AppliedConfig.Task
}
}

// Start the agent on the runtime broker
agentInfo, err := d.manager.Start(ctx, opts)
if err != nil {
Expand All @@ -1907,10 +1876,36 @@ func (d *agentDispatcherAdapter) DispatchAgentCreate(ctx context.Context, hubAge
// DispatchAgentStart implements hub.AgentDispatcher.
// For co-located runtime brokers, this resumes a stopped agent.
func (d *agentDispatcherAdapter) DispatchAgentStart(ctx context.Context, hubAgent *store.Agent, task string) error {
// For now, starting an existing agent is not fully supported in the manager
// The manager's Start method creates new agents, not resumes existing ones
// TODO: Implement proper agent resume functionality in the manager
log.Printf("DispatchAgentStart called for agent %s (not fully implemented)", hubAgent.Name)
grovePath := d.resolveGrovePath(ctx, hubAgent.GroveID)
opts := d.buildStartOptions(hubAgent, grovePath, true)

// Ensure grove ID label is present for tracking
if hubAgent.Labels == nil {
hubAgent.Labels = make(map[string]string)
}
hubAgent.Labels["scion.grove"] = hubAgent.GroveID
if task != "" {
opts.Task = task
}

// Start the agent on the runtime broker
agentInfo, err := d.manager.Start(ctx, opts)
if err != nil {
return fmt.Errorf("failed to start agent: %w", err)
}

// Update the hub agent record with runtime information
hubAgent.Phase = string(state.PhaseRunning)
hubAgent.ContainerStatus = agentInfo.ContainerStatus
if agentInfo.ID != "" {
hubAgent.RuntimeState = "container:" + agentInfo.ID
}
hubAgent.LastSeen = time.Now()

if err := d.store.UpdateAgent(ctx, hubAgent); err != nil {
log.Printf("Warning: failed to update agent with runtime info: %v", err)
}

return nil
}

Expand Down Expand Up @@ -1940,18 +1935,71 @@ func (d *agentDispatcherAdapter) DispatchAgentRestart(ctx context.Context, hubAg
log.Printf("Warning: failed to stop agent during restart: %v", err)
}

// TODO: Implement proper restart with start after stop
// For now, just update phase
grovePath := d.resolveGrovePath(ctx, hubAgent.GroveID)
opts := d.buildStartOptions(hubAgent, grovePath, true)

// Ensure grove ID label is present for tracking
if hubAgent.Labels == nil {
hubAgent.Labels = make(map[string]string)
}
hubAgent.Labels["scion.grove"] = hubAgent.GroveID

agentInfo, err := d.manager.Start(ctx, opts)
if err != nil {
return fmt.Errorf("failed to restart agent: %w", err)
}

hubAgent.Phase = string(state.PhaseRunning)
hubAgent.ContainerStatus = agentInfo.ContainerStatus
if agentInfo.ID != "" {
hubAgent.RuntimeState = "container:" + agentInfo.ID
}
hubAgent.LastSeen = time.Now()

if err := d.store.UpdateAgent(ctx, hubAgent); err != nil {
log.Printf("Warning: failed to update agent status: %v", err)
log.Printf("Warning: failed to update agent with runtime info: %v", err)
}

return nil
}

func (d *agentDispatcherAdapter) buildStartOptions(hubAgent *store.Agent, grovePath string, resume bool) api.StartOptions {
// Build StartOptions from the hub agent record
env := make(map[string]string)
if hubAgent.AppliedConfig != nil && hubAgent.AppliedConfig.Env != nil {
env = hubAgent.AppliedConfig.Env
}

opts := api.StartOptions{
Name: hubAgent.Name,
Template: hubAgent.Template,
Image: hubAgent.Image,
Env: env,
Detached: &hubAgent.Detached,
GrovePath: grovePath,
Resume: resume,
}

if hubAgent.AppliedConfig != nil {
opts.HarnessConfig = hubAgent.AppliedConfig.HarnessConfig
if hubAgent.AppliedConfig.Task != "" {
opts.Task = hubAgent.AppliedConfig.Task
}
}
return opts
}
func (d *agentDispatcherAdapter) resolveGrovePath(ctx context.Context, groveID string) string {
if groveID == "" || d.brokerID == "" {
return ""
}
provider, err := d.store.GetGroveProvider(ctx, groveID, d.brokerID)
if err != nil {
log.Printf("Warning: failed to get grove provider for path lookup: %v", err)
return ""
}
return provider.LocalPath
}

// DispatchAgentDelete implements hub.AgentDispatcher.
// It removes an agent from the runtime broker.
func (d *agentDispatcherAdapter) DispatchAgentDelete(ctx context.Context, hubAgent *store.Agent, deleteFiles, removeBranch, _ bool, _ time.Time) error {
Expand Down
Loading
Loading