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
6 changes: 3 additions & 3 deletions .github/workflows/security-release-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
env:
DAPPER_IMAGE: pasturestack/compose-cli-dapper:${{ github.sha }}
TRIVY_IMAGE: aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969
VERSION_OVERRIDE: 0.14.40
VERSION_OVERRIDE: 0.14.41
PLATFORM_COMPAT_JAR_URL: https://github.com/PastureStack/orchestration-engine/releases/download/v0.183.281/orchestration-engine-0.183.281.jar
PLATFORM_COMPAT_JAR_SHA256: da2a8a51562ed16e296f7e29e99482bb44042ff0834cca679bbe01d951ba1682

Expand Down Expand Up @@ -69,7 +69,7 @@ jobs:
}

run_ci
artifact="dist/artifacts/compose-executor-0.14.40-linux-amd64.gz"
artifact="dist/artifacts/compose-executor-0.14.41-linux-amd64.gz"
test -s "$artifact"
cp "$artifact" /tmp/compose-executor-first.gz

Expand All @@ -81,7 +81,7 @@ jobs:
mkdir -p evidence/product
gzip -cd "$artifact" > evidence/product/compose-executor
chmod +x evidence/product/compose-executor
evidence/product/compose-executor --version | grep -F '0.14.40' >/dev/null
evidence/product/compose-executor --version | grep -F '0.14.41' >/dev/null
cp "$artifact" evidence/
(
cd evidence
Expand Down
15 changes: 15 additions & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Compatibility Contract

## 0.14.41 source candidate: private Catalog request wire

The private Catalog preview producer emits business-only mutation inputs.
`setServiceLinks` always includes an array, including `[]` for no links, and
retains native target IDs and aliases. Service/Volume creation skeletons and
finalized child request structs exclude the generic SDK Resource envelope,
including nested launch configs. Business maps such as metadata, labels,
environment and log-driver options retain their exact keys and values.

This changes only the private preview serialization. Format version `1`,
normal parser/DTO owners, ordinary CLI/SDK serialization and Engine closed
validation remain unchanged. Qualification, publication and matching Server
runtime acceptance are pending. See the
[candidate note](docs/releases/compose-executor-0.14.41.md).

The migration preserves Docker Compose parsing, stack event names, generated stack fields, service labels, catalog question data, upgrade and rollback payloads, and compatibility API behavior.

The published v0.14.40 adds one server-private mode,
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ PastureStack is an independent community effort to preserve, audit, and moderniz

The maintained source uses Ubuntu 26.04, Go 1.27.3, and Python 3.14.9 for integration-source verification. Ubuntu build packages, the base image, and Go archives are fixed by snapshot, exact version, and SHA-256. Go dependencies are managed by `go.mod` and a reproducible module vendor tree. AWS access uses AWS SDK for Go v2; container structures use the maintained Moby API module; the command parser uses urfave/cli v3. The abandoned AWS SDK v1, Rancher generated-client modules, event-subscriber module, `docker/libcompose`, and other GOPATH-era dependencies have been removed. The small Rancher 1.6 protocol boundary retained by this compatibility CLI is maintained and tested locally. The Dapper builder does not embed a Docker CLI or mount a Docker socket; Docker is required only on the host to create and run the isolated builder. The maintained binaries are `pasturestack-compose` and `compose-executor`; `platform-compose.yml` is the preferred companion file. Releases are built and verified from the preserved source history; this repository does not enable automated deployment.

The [0.14.41 source candidate](docs/releases/compose-executor-0.14.41.md) fixes
the private Catalog launch wire format: mutation DTOs contain business fields,
and an empty link action explicitly carries `serviceLinks: []`. Native link
targets and aliases are preserved. Generic API resource links/actions are
excluded from these request structs; ordinary CLI and SDK requests retain
their behavior. Candidate qualification, publication and matching Server
runtime acceptance remain pending.

The current published component is
[v0.14.40](https://github.com/PastureStack/compose-cli/releases/tag/v0.14.40),
from producer `1835f8db645a8488e327b1f6e750bfdf3d1fb84d`. Its original CI archive
Expand Down
41 changes: 27 additions & 14 deletions cataloglaunch/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,18 @@ type Inspect struct {
}

type Service struct {
Name string `json:"name"`
PrimaryName string `json:"primaryName,omitempty"`
ResourceType string `json:"resourceType,omitempty"`
CreateSkeleton *platformapi.CompositeService `json:"createSkeleton,omitempty"`
Config *config.ServiceConfig `json:"config"`
Inspect Inspect `json:"dockerInspect"`
Name string `json:"name"`
PrimaryName string `json:"primaryName,omitempty"`
ResourceType string `json:"resourceType,omitempty"`
CreateSkeleton map[string]interface{} `json:"createSkeleton,omitempty"`
Config *config.ServiceConfig `json:"config"`
Inspect Inspect `json:"dockerInspect"`
}

type VolumeRequest struct {
Name string `json:"name"`
ResourceType string `json:"resourceType"`
CreateSkeleton *client.VolumeTemplate `json:"createSkeleton"`
CreateSkeleton map[string]interface{} `json:"createSkeleton"`
}

type Output struct {
Expand All @@ -83,10 +83,10 @@ type Output struct {
}

type ServiceRequest struct {
Name string `json:"name"`
ResourceType string `json:"resourceType"`
CreateBody interface{} `json:"createBody"`
SetServiceLinks *client.SetServiceLinksInput `json:"setServiceLinks"`
Name string `json:"name"`
ResourceType string `json:"resourceType"`
CreateBody map[string]interface{} `json:"createBody"`
SetServiceLinks *serviceLinksWire `json:"setServiceLinks"`
}

type catalogEnvironment struct {
Expand Down Expand Up @@ -221,7 +221,11 @@ func Prepare(input Input) (*Output, error) {
if !namePattern.MatchString(name) {
return nil, ErrUnsupported
}
result.VolumeRequests = append(result.VolumeRequests, VolumeRequest{Name: name, ResourceType: "volumeTemplate", CreateSkeleton: platformapi.CatalogVolumeCreateSkeleton(name, p.VolumeConfigs[name])})
skeleton, err := catalogRequestWire(platformapi.CatalogVolumeCreateSkeleton(name, p.VolumeConfigs[name]))
if err != nil {
return nil, err
}
result.VolumeRequests = append(result.VolumeRequests, VolumeRequest{Name: name, ResourceType: "volumeTemplate", CreateSkeleton: skeleton})
}
for _, name := range names {
if !namePattern.MatchString(name) {
Expand Down Expand Up @@ -275,7 +279,12 @@ func Prepare(input Input) (*Output, error) {
h.NetworkMode = ""
entry := Service{Name: name, PrimaryName: primaries[name], Config: canonical, Inspect: Inspect{Name: "/" + name, Config: c, HostConfig: h}}
if entry.PrimaryName == "" {
entry.ResourceType, entry.CreateSkeleton = platformapi.CatalogServiceCreateSkeleton(name, &copy)
kind, skeleton := platformapi.CatalogServiceCreateSkeleton(name, &copy)
wire, err := catalogRequestWire(skeleton)
if err != nil {
return nil, err
}
entry.ResourceType, entry.CreateSkeleton = kind, wire
}
result.Services = append(result.Services, entry)
}
Expand Down Expand Up @@ -326,7 +335,11 @@ func Prepare(input Input) (*Output, error) {
if kind == "externalService" {
links = nil
}
result.ServiceRequests = append(result.ServiceRequests, ServiceRequest{Name: entry.Name, ResourceType: kind, CreateBody: platformapi.CatalogServiceRequestPayload(kind, body), SetServiceLinks: links})
wire, err := catalogRequestWire(platformapi.CatalogServiceRequestPayload(kind, body))
if err != nil {
return nil, err
}
result.ServiceRequests = append(result.ServiceRequests, ServiceRequest{Name: entry.Name, ResourceType: kind, CreateBody: wire, SetServiceLinks: catalogLinksWire(links)})
}
result.Phase = "finalize"
}
Expand Down
109 changes: 109 additions & 0 deletions cataloglaunch/wire.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package cataloglaunch

import (
"bytes"
"encoding/json"
"reflect"
"strings"

client "github.com/PastureStack/compose-cli/internal/rancherclient/v2"
)

// These are mutation inputs, not generic API resources. Empty links remain an
// explicit business value, and link aliases/targets retain the native parser's values.
type serviceLinkWire struct {
Name string `json:"name,omitempty"`
ServiceID string `json:"serviceId"`
}

type serviceLinksWire struct {
ServiceLinks []serviceLinkWire `json:"serviceLinks"`
}

func catalogLinksWire(input *client.SetServiceLinksInput) *serviceLinksWire {
if input == nil {
return nil
}
result := &serviceLinksWire{ServiceLinks: make([]serviceLinkWire, 0, len(input.ServiceLinks))}
for _, link := range input.ServiceLinks {
result.ServiceLinks = append(result.ServiceLinks, serviceLinkWire{Name: link.Name, ServiceID: link.ServiceId})
}
return result
}

var apiResourceType = reflect.TypeOf(client.Resource{})

// Only this private preview producer projects SDK request structs. Preserve the
// native JSON encoding, then omit the SDK's typed Resource envelope throughout
// those structs. Maps are business data and are never traversed or key-stripped.
// Ordinary SDK/legacy requests keep their original serialization.
func catalogRequestWire(value interface{}) (map[string]interface{}, error) {
raw, err := json.Marshal(value)
if err != nil {
return nil, ErrUnsupported
}
var result map[string]interface{}
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.UseNumber()
if decoder.Decode(&result) != nil || result == nil {
return nil, ErrUnsupported
}
projectRequestStruct(reflect.ValueOf(value), result)
return result, nil
}

func projectRequestStruct(value reflect.Value, encoded interface{}) {
for value.IsValid() && (value.Kind() == reflect.Ptr || value.Kind() == reflect.Interface) {
if value.IsNil() {
return
}
value = value.Elem()
}
if !value.IsValid() {
return
}
switch value.Kind() {
case reflect.Struct:
object, ok := encoded.(map[string]interface{})
if !ok {
return
}
if value.Type() == apiResourceType {
// A named Resource property is data, not a promoted response envelope.
return
}
for index := 0; index < value.NumField(); index++ {
field := value.Type().Field(index)
name := strings.Split(field.Tag.Get("json"), ",")[0]
if field.PkgPath != "" || name == "-" {
continue
}
if field.Anonymous && name == "" {
embedded := field.Type
if embedded.Kind() == reflect.Ptr {
embedded = embedded.Elem()
}
if embedded == apiResourceType {
for resourceIndex := 0; resourceIndex < apiResourceType.NumField(); resourceIndex++ {
key := strings.Split(apiResourceType.Field(resourceIndex).Tag.Get("json"), ",")[0]
delete(object, key)
}
} else {
projectRequestStruct(value.Field(index), object)
}
} else {
if name == "" {
name = field.Name
}
projectRequestStruct(value.Field(index), object[name])
}
}
case reflect.Array, reflect.Slice:
items, ok := encoded.([]interface{})
if ok {
for index := 0; index < value.Len() && index < len(items); index++ {
projectRequestStruct(value.Index(index), items[index])
}
}
}
}
Loading
Loading