Skip to content
Merged
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
24 changes: 24 additions & 0 deletions internal/artifactcrypto/artifactcrypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,30 @@ func (h Header) CheckKey(key []byte) error {
return nil
}

// IsEncrypted reports whether src begins with the v1 payload marker. It reads
// only the leading length prefix and magic, so a non-payload stream (or one too
// short to be a payload) returns false without error. A stream whose marker
// matches but whose body is malformed still returns true here; that is caught
// when the payload is actually parsed or decrypted, so detection and validation
// stay distinct.
func IsEncrypted(src io.Reader) (bool, error) {
var lenBuf [4]byte
if _, err := io.ReadFull(src, lenBuf[:]); err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
return false, nil
}
return false, errs.Wrap(err, "reading payload prefix")
}
magic := make([]byte, len(magicMarker))
if _, err := io.ReadFull(src, magic); err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
return false, nil
}
return false, errs.Wrap(err, "reading payload marker")
}
return string(magic) == magicMarker, nil
}

// ParseHeader reads the header from src, consuming exactly the header bytes and
// leaving src positioned at the first chunk.
func ParseHeader(src io.Reader) (Header, error) {
Expand Down
27 changes: 27 additions & 0 deletions internal/artifactcrypto/artifactcrypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,33 @@ func TestRoundTripPreservesZip(t *testing.T) {
}
}

func TestIsEncrypted(t *testing.T) {
payload := encryptToBytes(t, []byte("secret"), "kid")

cases := []struct {
name string
data []byte
want bool
}{
{"encrypted payload", payload, true},
{"json plaintext", []byte(`{"schema":"x"}`), false},
{"short input", []byte{0x00, 0x01}, false},
{"empty", nil, false},
{"marker present but truncated body", payload[:4+len(magicMarker)], true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := IsEncrypted(bytes.NewReader(tc.data))
if err != nil {
t.Fatalf("IsEncrypted: %v", err)
}
if got != tc.want {
t.Errorf("IsEncrypted = %v, want %v", got, tc.want)
}
})
}
}

func TestFingerprint(t *testing.T) {
fp := Fingerprint(testKey)
if len(fp) != len("sha256:")+64 {
Expand Down
14 changes: 14 additions & 0 deletions internal/runbits/runtime/progress/progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,20 @@ func (p *ProgressDigester) Handle(ev events.Event) error {
}
p.installBar.Increment()

case events.ArtifactInstallSkipped:
// A skipped artifact fires no Started event, so create the bar on demand,
// then advance it as if installed so it still reaches completion.
if p.installBar == nil {
p.installBar = p.addTotalBar(locale.Tl("progress_installing", "Installing"), int64(len(p.installsExpected)), mpb.BarPriority(StepInstall.priority))
}
if _, ok := p.installsExpected[v.ArtifactID]; !ok {
return errs.New("ArtifactInstallSkipped called for an artifact that was not expected: %s", v.ArtifactID.String())
}
if p.installBar.Current() == p.installBar.total {
return errs.New("Install bar is already complete, this should not happen")
}
p.installBar.Increment()

}

return nil
Expand Down
58 changes: 58 additions & 0 deletions internal/runbits/runtime/progress/progress_skip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package progress

import (
"io"
"testing"

"github.com/ActiveState/cli/internal/testhelpers/outputhelper"
"github.com/ActiveState/cli/pkg/buildplan"
"github.com/ActiveState/cli/pkg/runtime/events"
"github.com/go-openapi/strfmt"
)

// A skipped artifact fires only ArtifactInstallSkipped (no Started/Success), yet
// it is counted in the install bar's total. The bar must still reach its total,
// otherwise Close() stalls waiting for it and reports a spurious error.
func TestInstallSkippedCompletesBar(t *testing.T) {
installed := strfmt.UUID("11111111-1111-1111-1111-111111111111")
skipped := strfmt.UUID("22222222-2222-2222-2222-222222222222")

t.Run("alongside an installed artifact", func(t *testing.T) {
p := newProgressIndicator(io.Discard, outputhelper.NewCatcher())
defer p.cancelMpb()

expected := buildplan.ArtifactIDMap{installed: nil, skipped: nil}
handle(t, p, events.Start{ArtifactsToInstall: expected})
handle(t, p, events.ArtifactInstallStarted{installed})
handle(t, p, events.ArtifactInstallSuccess{installed})
handle(t, p, events.ArtifactInstallSkipped{skipped, "pkg"})

if got, want := p.installBar.Current(), int64(len(expected)); got != want {
t.Errorf("install bar at %d/%d; want %d (a skipped artifact must still advance the bar)", got, p.installBar.total, want)
}
})

t.Run("when every artifact is skipped", func(t *testing.T) {
p := newProgressIndicator(io.Discard, outputhelper.NewCatcher())
defer p.cancelMpb()

// No Started event fires, so the skip event itself must create the bar.
expected := buildplan.ArtifactIDMap{skipped: nil}
handle(t, p, events.Start{ArtifactsToInstall: expected})
handle(t, p, events.ArtifactInstallSkipped{skipped, "pkg"})

if p.installBar == nil {
t.Fatal("install bar was never created for an all-skipped install")
}
if got, want := p.installBar.Current(), int64(len(expected)); got != want {
t.Errorf("install bar at %d/%d; want %d", got, p.installBar.total, want)
}
})
}

func handle(t *testing.T, p *ProgressDigester, ev events.Event) {
t.Helper()
if err := p.Handle(ev); err != nil {
t.Fatalf("Handle(%T): %v", ev, err)
}
}
44 changes: 42 additions & 2 deletions internal/runbits/runtime/runtime.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package runtime_runbit

import (
"context"
"fmt"
"net/url"
"os"
"strings"
"sync"

anaConsts "github.com/ActiveState/cli/internal/analytics/constants"
"github.com/ActiveState/cli/internal/analytics/dimensions"
Expand All @@ -22,6 +24,7 @@ import (
"github.com/ActiveState/cli/internal/rtutils/ptr"
buildscript_runbit "github.com/ActiveState/cli/internal/runbits/buildscript"
"github.com/ActiveState/cli/internal/runbits/checkout"
"github.com/ActiveState/cli/internal/runbits/orgkey"
"github.com/ActiveState/cli/internal/runbits/rationalize"
"github.com/ActiveState/cli/internal/runbits/runtime/progress"
"github.com/ActiveState/cli/internal/runbits/runtime/trigger"
Expand All @@ -36,7 +39,6 @@ import (
"github.com/ActiveState/cli/pkg/runtime_helpers"
"github.com/ActiveState/cli/pkg/sysinfo"
"github.com/go-openapi/strfmt"
"golang.org/x/net/context"
)

func init() {
Expand Down Expand Up @@ -253,9 +255,11 @@ func Update(
pg := progress.NewRuntimeProgressIndicator(prime.Output())
defer rtutils.Closer(pg.Close, &rerr)

skipped := &skipReporter{}

rtOpts := []runtime.SetOpt{
runtime.WithAnnotations(proj.Owner(), proj.Name(), commitID),
runtime.WithEventHandlers(pg.Handle, ah.handle),
runtime.WithEventHandlers(pg.Handle, ah.handle, skipped.handle),
runtime.WithPreferredLibcVersion(prime.Config().GetString(constants.PreferredGlibcVersionConfig)),
runtime.WithAuthToken(prime.Auth().BearerToken()),
}
Expand Down Expand Up @@ -285,6 +289,20 @@ func Update(
}
rtOpts = append(rtOpts, runtime.WithCacheSize(prime.Config().GetInt(constants.RuntimeCacheSizeConfigKey)))

// Fetch the organization key for private ingredients, if a key service is configured.
orgKeyProvider := orgkey.New(prime.Config(), proj.Owner())
if orgKeyProvider.Configured() {
defer orgKeyProvider.Close()
key, keyID, err := orgKeyProvider.Key(context.Background())
if err != nil {
prime.Output().Notice(locale.Tl("warn_orgkey_unavailable",
"[WARNING]Warning:[/RESET] Could not fetch the organization key: {{.V0}}. Encrypted private artifacts will be skipped. Ensure the key is available and run '[ACTIONABLE]state refresh[/RESET]' to try installing them again.",
errs.JoinMessage(err)))
} else {
rtOpts = append(rtOpts, runtime.WithDecryptionKey(key, keyID))
}
}

if isArmPlatform(buildPlan) {
prime.Output().Notice(locale.Tl("warning_arm_unstable", "[WARNING]Warning:[/RESET] You are using an ARM64 architecture, which is currently unstable. While it may work, you might encounter issues."))
}
Expand All @@ -293,6 +311,12 @@ func Update(
return nil, locale.WrapError(err, "err_packages_update_runtime_install")
}

if len(skipped.names) > 0 {
prime.Output().Notice(locale.Tl("warn_private_artifacts_skipped",
"[WARNING]Warning:[/RESET] These private packages were skipped because the organization key was unavailable: {{.V0}}. Ensure the key is available and run '[ACTIONABLE]state refresh[/RESET]' to try installing them again.",
strings.Join(skipped.names, ", ")))
}

return rt, nil
}

Expand Down Expand Up @@ -405,3 +429,19 @@ func (h *analyticsHandler) handle(event events.Event) error {

return nil
}

// skipReporter collects the names of artifacts skipped during runtime setup so
// the caller can report them once the update completes.
type skipReporter struct {
mutex sync.Mutex
names []string
}

func (r *skipReporter) handle(event events.Event) error {
if e, ok := event.(events.ArtifactInstallSkipped); ok {
r.mutex.Lock()
defer r.mutex.Unlock()
r.names = append(r.names, e.Name)
}
return nil
}
Loading
Loading