Skip to content

Commit d00185d

Browse files
authored
Merge branch 'main' into main
2 parents b2af545 + 114ba45 commit d00185d

File tree

77 files changed

+2604
-1295
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+2604
-1295
lines changed

.github/workflows/ci.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,33 @@ jobs:
11801180
done
11811181
fi
11821182
1183+
- name: SBOM Generation and Attestation
1184+
if: github.ref == 'refs/heads/main'
1185+
env:
1186+
COSIGN_EXPERIMENTAL: 1
1187+
run: |
1188+
set -euxo pipefail
1189+
1190+
# Define image base and tags
1191+
IMAGE_BASE="ghcr.io/coder/coder-preview"
1192+
TAGS=("${{ steps.build-docker.outputs.tag }}" "main" "latest")
1193+
1194+
# Generate and attest SBOM for each tag
1195+
for tag in "${TAGS[@]}"; do
1196+
IMAGE="${IMAGE_BASE}:${tag}"
1197+
SBOM_FILE="coder_sbom_${tag//[:\/]/_}.spdx.json"
1198+
1199+
echo "Generating SBOM for image: ${IMAGE}"
1200+
syft "${IMAGE}" -o spdx-json > "${SBOM_FILE}"
1201+
1202+
echo "Attesting SBOM to image: ${IMAGE}"
1203+
cosign clean "${IMAGE}"
1204+
cosign attest --type spdxjson \
1205+
--predicate "${SBOM_FILE}" \
1206+
--yes \
1207+
"${IMAGE}"
1208+
done
1209+
11831210
# GitHub attestation provides SLSA provenance for the Docker images, establishing a verifiable
11841211
# record that these images were built in GitHub Actions with specific inputs and environment.
11851212
# This complements our existing cosign attestations which focus on SBOMs.

.github/workflows/release.yaml

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,39 @@ jobs:
501501
env:
502502
CODER_BASE_IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }}
503503

504+
- name: SBOM Generation and Attestation
505+
if: ${{ !inputs.dry_run }}
506+
env:
507+
COSIGN_EXPERIMENTAL: "1"
508+
run: |
509+
set -euxo pipefail
510+
511+
# Generate SBOM for multi-arch image with version in filename
512+
echo "Generating SBOM for multi-arch image: ${{ steps.build_docker.outputs.multiarch_image }}"
513+
syft "${{ steps.build_docker.outputs.multiarch_image }}" -o spdx-json > coder_${{ steps.version.outputs.version }}_sbom.spdx.json
514+
515+
# Attest SBOM to multi-arch image
516+
echo "Attesting SBOM to multi-arch image: ${{ steps.build_docker.outputs.multiarch_image }}"
517+
cosign clean "${{ steps.build_docker.outputs.multiarch_image }}"
518+
cosign attest --type spdxjson \
519+
--predicate coder_${{ steps.version.outputs.version }}_sbom.spdx.json \
520+
--yes \
521+
"${{ steps.build_docker.outputs.multiarch_image }}"
522+
523+
# If latest tag was created, also attest it
524+
if [[ "${{ steps.build_docker.outputs.created_latest_tag }}" == "true" ]]; then
525+
latest_tag="$(./scripts/image_tag.sh --version latest)"
526+
echo "Generating SBOM for latest image: ${latest_tag}"
527+
syft "${latest_tag}" -o spdx-json > coder_latest_sbom.spdx.json
528+
529+
echo "Attesting SBOM to latest image: ${latest_tag}"
530+
cosign clean "${latest_tag}"
531+
cosign attest --type spdxjson \
532+
--predicate coder_latest_sbom.spdx.json \
533+
--yes \
534+
"${latest_tag}"
535+
fi
536+
504537
- name: GitHub Attestation for Docker image
505538
id: attest_main
506539
if: ${{ !inputs.dry_run }}
@@ -617,16 +650,27 @@ jobs:
617650
fi
618651
declare -p publish_args
619652
653+
# Build the list of files to publish
654+
files=(
655+
./build/*_installer.exe
656+
./build/*.zip
657+
./build/*.tar.gz
658+
./build/*.tgz
659+
./build/*.apk
660+
./build/*.deb
661+
./build/*.rpm
662+
./coder_${{ steps.version.outputs.version }}_sbom.spdx.json
663+
)
664+
665+
# Only include the latest SBOM file if it was created
666+
if [[ "${{ steps.build_docker.outputs.created_latest_tag }}" == "true" ]]; then
667+
files+=(./coder_latest_sbom.spdx.json)
668+
fi
669+
620670
./scripts/release/publish.sh \
621671
"${publish_args[@]}" \
622672
--release-notes-file "$CODER_RELEASE_NOTES_FILE" \
623-
./build/*_installer.exe \
624-
./build/*.zip \
625-
./build/*.tar.gz \
626-
./build/*.tgz \
627-
./build/*.apk \
628-
./build/*.deb \
629-
./build/*.rpm
673+
"${files[@]}"
630674
env:
631675
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
632676
CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }}
@@ -668,6 +712,15 @@ jobs:
668712
./build/*.apk
669713
./build/*.deb
670714
./build/*.rpm
715+
./coder_${{ steps.version.outputs.version }}_sbom.spdx.json
716+
retention-days: 7
717+
718+
- name: Upload latest sbom artifact to actions (if dry-run)
719+
if: inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true'
720+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
721+
with:
722+
name: latest-sbom-artifact
723+
path: ./coder_latest_sbom.spdx.json
671724
retention-days: 7
672725

673726
- name: Send repository-dispatch event

agent/agent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1408,7 +1408,7 @@ func (a *agent) createTailnet(
14081408
if rPTYServeErr != nil &&
14091409
a.gracefulCtx.Err() == nil &&
14101410
!strings.Contains(rPTYServeErr.Error(), "use of closed network connection") {
1411-
a.logger.Error(ctx, "error serving reconnecting PTY", slog.Error(err))
1411+
a.logger.Error(ctx, "error serving reconnecting PTY", slog.Error(rPTYServeErr))
14121412
}
14131413
}); err != nil {
14141414
return nil, err

agent/agentssh/agentssh.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1060,8 +1060,10 @@ func (s *Server) Close() error {
10601060
// Guard against multiple calls to Close and
10611061
// accepting new connections during close.
10621062
if s.closing != nil {
1063+
closing := s.closing
10631064
s.mu.Unlock()
1064-
return xerrors.New("server is closing")
1065+
<-closing
1066+
return xerrors.New("server is closed")
10651067
}
10661068
s.closing = make(chan struct{})
10671069

agent/agentssh/agentssh_test.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ func TestNewServer_CloseActiveConnections(t *testing.T) {
153153
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
154154
s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil)
155155
require.NoError(t, err)
156-
defer s.Close()
156+
t.Cleanup(func() {
157+
_ = s.Close()
158+
})
157159
err = s.UpdateHostSigner(42)
158160
assert.NoError(t, err)
159161

@@ -190,10 +192,17 @@ func TestNewServer_CloseActiveConnections(t *testing.T) {
190192
}
191193
// The 60 seconds here is intended to be longer than the
192194
// test. The shutdown should propagate.
193-
err = sess.Start("/bin/bash -c 'trap \"sleep 60\" SIGTERM; sleep 60'")
195+
if runtime.GOOS == "windows" {
196+
// Best effort to at least partially test this in Windows.
197+
err = sess.Start("echo start\"ed\" && sleep 60")
198+
} else {
199+
err = sess.Start("/bin/bash -c 'trap \"sleep 60\" SIGTERM; echo start\"ed\"; sleep 60'")
200+
}
194201
assert.NoError(t, err)
195202

203+
pty.ExpectMatchContext(ctx, "started")
196204
close(ch)
205+
197206
err = sess.Wait()
198207
assert.Error(t, err)
199208
}(waitConns[i])

agent/agentssh/exec_windows.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package agentssh
22

33
import (
44
"context"
5-
"os"
65
"os/exec"
76
"syscall"
87

@@ -15,7 +14,12 @@ func cmdSysProcAttr() *syscall.SysProcAttr {
1514

1615
func cmdCancel(ctx context.Context, logger slog.Logger, cmd *exec.Cmd) func() error {
1716
return func() error {
18-
logger.Debug(ctx, "cmdCancel: sending interrupt to process", slog.F("pid", cmd.Process.Pid))
19-
return cmd.Process.Signal(os.Interrupt)
17+
logger.Debug(ctx, "cmdCancel: killing process", slog.F("pid", cmd.Process.Pid))
18+
// Windows doesn't support sending signals to process groups, so we
19+
// have to kill the process directly. In the future, we may want to
20+
// implement a more sophisticated solution for process groups on
21+
// Windows, but for now, this is a simple way to ensure that the
22+
// process is terminated when the context is cancelled.
23+
return cmd.Process.Kill()
2024
}
2125
}

cli/configssh.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ const (
4545
// sshConfigOptions represents options that can be stored and read
4646
// from the coder config in ~/.ssh/coder.
4747
type sshConfigOptions struct {
48-
waitEnum string
48+
waitEnum string
49+
// Deprecated: moving away from prefix to hostnameSuffix
4950
userHostPrefix string
51+
hostnameSuffix string
5052
sshOptions []string
5153
disableAutostart bool
5254
header []string
@@ -97,7 +99,11 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool {
9799
if !slicesSortedEqual(o.header, other.header) {
98100
return false
99101
}
100-
return o.waitEnum == other.waitEnum && o.userHostPrefix == other.userHostPrefix && o.disableAutostart == other.disableAutostart && o.headerCommand == other.headerCommand
102+
return o.waitEnum == other.waitEnum &&
103+
o.userHostPrefix == other.userHostPrefix &&
104+
o.disableAutostart == other.disableAutostart &&
105+
o.headerCommand == other.headerCommand &&
106+
o.hostnameSuffix == other.hostnameSuffix
101107
}
102108

103109
// slicesSortedEqual compares two slices without side-effects or regard to order.
@@ -119,6 +125,9 @@ func (o sshConfigOptions) asList() (list []string) {
119125
if o.userHostPrefix != "" {
120126
list = append(list, fmt.Sprintf("ssh-host-prefix: %s", o.userHostPrefix))
121127
}
128+
if o.hostnameSuffix != "" {
129+
list = append(list, fmt.Sprintf("hostname-suffix: %s", o.hostnameSuffix))
130+
}
122131
if o.disableAutostart {
123132
list = append(list, fmt.Sprintf("disable-autostart: %v", o.disableAutostart))
124133
}
@@ -314,6 +323,10 @@ func (r *RootCmd) configSSH() *serpent.Command {
314323
// Override with user flag.
315324
coderdConfig.HostnamePrefix = sshConfigOpts.userHostPrefix
316325
}
326+
if sshConfigOpts.hostnameSuffix != "" {
327+
// Override with user flag.
328+
coderdConfig.HostnameSuffix = sshConfigOpts.hostnameSuffix
329+
}
317330

318331
// Write agent configuration.
319332
defaultOptions := []string{
@@ -518,6 +531,12 @@ func (r *RootCmd) configSSH() *serpent.Command {
518531
Description: "Override the default host prefix.",
519532
Value: serpent.StringOf(&sshConfigOpts.userHostPrefix),
520533
},
534+
{
535+
Flag: "hostname-suffix",
536+
Env: "CODER_CONFIGSSH_HOSTNAME_SUFFIX",
537+
Description: "Override the default hostname suffix.",
538+
Value: serpent.StringOf(&sshConfigOpts.hostnameSuffix),
539+
},
521540
{
522541
Flag: "wait",
523542
Env: "CODER_CONFIGSSH_WAIT", // Not to be mixed with CODER_SSH_WAIT.
@@ -568,6 +587,9 @@ func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOption
568587
if o.userHostPrefix != "" {
569588
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-host-prefix", o.userHostPrefix)
570589
}
590+
if o.hostnameSuffix != "" {
591+
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "hostname-suffix", o.hostnameSuffix)
592+
}
571593
if o.disableAutostart {
572594
_, _ = fmt.Fprintf(&ow, "# :%s=%v\n", "disable-autostart", o.disableAutostart)
573595
}
@@ -607,6 +629,8 @@ func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) {
607629
o.waitEnum = parts[1]
608630
case "ssh-host-prefix":
609631
o.userHostPrefix = parts[1]
632+
case "hostname-suffix":
633+
o.hostnameSuffix = parts[1]
610634
case "ssh-option":
611635
o.sshOptions = append(o.sshOptions, parts[1])
612636
case "disable-autostart":

cli/configssh_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
432432
"# Last config-ssh options:",
433433
"# :wait=yes",
434434
"# :ssh-host-prefix=coder-test.",
435+
"# :hostname-suffix=coder-suffix",
435436
"# :header=X-Test-Header=foo",
436437
"# :header=X-Test-Header2=bar",
437438
"# :header-command=printf h1=v1 h2=\"v2\" h3='v3'",
@@ -447,6 +448,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
447448
"--yes",
448449
"--wait=yes",
449450
"--ssh-host-prefix", "coder-test.",
451+
"--hostname-suffix", "coder-suffix",
450452
"--header", "X-Test-Header=foo",
451453
"--header", "X-Test-Header2=bar",
452454
"--header-command", "printf h1=v1 h2=\"v2\" h3='v3'",

cli/server.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,15 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
620620
return xerrors.Errorf("parse ssh config options %q: %w", vals.SSHConfig.SSHConfigOptions.String(), err)
621621
}
622622

623+
// The workspace hostname suffix is always interpreted as implicitly beginning with a single dot, so it is
624+
// a config error to explicitly include the dot. This ensures that we always interpret the suffix as a
625+
// separate DNS label, and not just an ordinary string suffix. E.g. a suffix of 'coder' will match
626+
// 'en.coder' but not 'encoder'.
627+
if strings.HasPrefix(vals.WorkspaceHostnameSuffix.String(), ".") {
628+
return xerrors.Errorf("you must omit any leading . in workspace hostname suffix: %s",
629+
vals.WorkspaceHostnameSuffix.String())
630+
}
631+
623632
options := &coderd.Options{
624633
AccessURL: vals.AccessURL.Value(),
625634
AppHostname: appHostname,
@@ -653,6 +662,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
653662
SSHConfig: codersdk.SSHConfigResponse{
654663
HostnamePrefix: vals.SSHConfig.DeploymentName.String(),
655664
SSHConfigOptions: configSSHOptions,
665+
HostnameSuffix: vals.WorkspaceHostnameSuffix.String(),
656666
},
657667
AllowWorkspaceRenames: vals.AllowWorkspaceRenames.Value(),
658668
Entitlements: entitlements.New(),

0 commit comments

Comments
 (0)