feat: add spec definition api with versioning
This commit is contained in:
@ -38,7 +38,7 @@ func (c *Controller) Name() string {
|
||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
appSpec := spec.NewSpec()
|
||||
|
||||
if err := state.GetSpec(spec.Name, appSpec); err != nil {
|
||||
if err := state.GetSpec(spec.Name, spec.Version, appSpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find app spec")
|
||||
|
||||
@ -50,7 +50,12 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "retrieved spec", logger.F("spec", appSpec.SpecName()), logger.F("revision", appSpec.SpecRevision()))
|
||||
logger.Info(
|
||||
ctx, "retrieved spec",
|
||||
logger.F("name", appSpec.SpecDefinitionName()),
|
||||
logger.F("version", appSpec.SpecDefinitionVersion()),
|
||||
logger.F("revision", appSpec.SpecRevision()),
|
||||
)
|
||||
|
||||
c.updateApps(ctx, appSpec)
|
||||
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
var schema []byte
|
||||
|
||||
func init() {
|
||||
if err := spec.Register(Name, schema); err != nil {
|
||||
if err := spec.Register(string(Name), Version, schema); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"$id": "https://app.edge.emissary.cadoles.com/spec.json",
|
||||
"title": "AppSpec",
|
||||
"description": "Emissary 'App' specification",
|
||||
@ -78,7 +78,9 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["defaultUrlTemplate"],
|
||||
"required": [
|
||||
"defaultUrlTemplate"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"unexpectedHostRedirect": {
|
||||
@ -94,7 +96,10 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["acceptedHostPatterns", "hostTarget"],
|
||||
"required": [
|
||||
"acceptedHostPatterns",
|
||||
"hostTarget"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"auth": {
|
||||
@ -104,7 +109,10 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": ["object", "string"]
|
||||
"type": [
|
||||
"object",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"signingAlgorithm": {
|
||||
"type": "string"
|
||||
|
@ -6,7 +6,10 @@ import (
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
)
|
||||
|
||||
const Name spec.Name = "app.emissary.cadoles.com"
|
||||
const (
|
||||
Name string = "app.emissary.cadoles.com"
|
||||
Version string = "0.0.0"
|
||||
)
|
||||
|
||||
type Spec struct {
|
||||
Revision int `json:"revision"`
|
||||
@ -56,10 +59,14 @@ type AppURLResolving struct {
|
||||
DefaultURLTemplate string `json:"defaultUrlTemplate"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
func (s *Spec) SpecDefinitionName() string {
|
||||
return Name
|
||||
}
|
||||
|
||||
func (s *Spec) SpecDefinitionVersion() string {
|
||||
return Version
|
||||
}
|
||||
|
||||
func (s *Spec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore/memory"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -27,11 +28,15 @@ var validatorTestCases = []validatorTestCase{
|
||||
func TestValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validator := spec.NewValidator()
|
||||
if err := validator.Register(Name, schema); err != nil {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := memory.NewSpecDefinitionRepository()
|
||||
if _, err := repo.Upsert(ctx, Name, Version, schema); err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
validator := spec.NewValidator(repo)
|
||||
|
||||
for _, tc := range validatorTestCases {
|
||||
func(tc validatorTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
|
@ -33,7 +33,7 @@ func (c *Controller) Name() string {
|
||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
mdnsSpec := mdns.NewSpec()
|
||||
|
||||
if err := state.GetSpec(mdns.Name, mdnsSpec); err != nil {
|
||||
if err := state.GetSpec(mdns.Name, mdns.Version, mdnsSpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find mdns spec")
|
||||
|
||||
@ -45,7 +45,11 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "retrieved spec", logger.F("spec", mdnsSpec.SpecName()), logger.F("revision", mdnsSpec.SpecRevision()))
|
||||
logger.Info(ctx, "retrieved spec",
|
||||
logger.F("name", mdnsSpec.SpecDefinitionName()),
|
||||
logger.F("version", mdnsSpec.SpecDefinitionVersion()),
|
||||
logger.F("revision", mdnsSpec.SpecRevision()),
|
||||
)
|
||||
|
||||
if err := c.updateResponder(ctx, mdnsSpec); err != nil {
|
||||
return errors.Wrap(err, "could not update responder")
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
var schema []byte
|
||||
|
||||
func init() {
|
||||
if err := spec.Register(Name, schema); err != nil {
|
||||
if err := spec.Register(string(Name), Version, schema); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"$id": "https://mdns.edge.emissary.cadoles.com/spec.json",
|
||||
"title": "MDNSSpec",
|
||||
"description": "Emissary 'MDNS' specification",
|
||||
|
@ -4,7 +4,10 @@ import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
)
|
||||
|
||||
const Name spec.Name = "mdns.emissary.cadoles.com"
|
||||
const (
|
||||
Name string = "mdns.emissary.cadoles.com"
|
||||
Version string = "0.0.0"
|
||||
)
|
||||
|
||||
type Spec struct {
|
||||
Revision int `json:"revision"`
|
||||
@ -19,10 +22,14 @@ type Service struct {
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
func (s *Spec) SpecDefinitionName() string {
|
||||
return Name
|
||||
}
|
||||
|
||||
func (s *Spec) SpecDefinitionVersion() string {
|
||||
return Version
|
||||
}
|
||||
|
||||
func (s *Spec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore/memory"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -27,11 +28,15 @@ var validatorTestCases = []validatorTestCase{
|
||||
func TestValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validator := spec.NewValidator()
|
||||
if err := validator.Register(Name, schema); err != nil {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := memory.NewSpecDefinitionRepository()
|
||||
if _, err := repo.Upsert(ctx, Name, Version, schema); err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
validator := spec.NewValidator(repo)
|
||||
|
||||
for _, tc := range validatorTestCases {
|
||||
func(tc validatorTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
var schema []byte
|
||||
|
||||
func init() {
|
||||
if err := spec.Register(Name, schema); err != nil {
|
||||
if err := spec.Register(string(Name), Version, schema); err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"$id": "https://sysupgrade.openwrt.emissary.cadoles.com/spec.json",
|
||||
"title": "SysUpgradeSpec",
|
||||
"description": "Emissary 'SysUpgrade' specification",
|
||||
@ -15,6 +15,10 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["url", "sha256sum", "version"],
|
||||
"required": [
|
||||
"url",
|
||||
"sha256sum",
|
||||
"version"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
@ -4,7 +4,10 @@ import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
)
|
||||
|
||||
const Name spec.Name = "sysupgrade.openwrt.emissary.cadoles.com"
|
||||
const (
|
||||
Name string = "sysupgrade.openwrt.emissary.cadoles.com"
|
||||
Version string = "0.0.0"
|
||||
)
|
||||
|
||||
type Spec struct {
|
||||
Revision int `json:"revision"`
|
||||
@ -13,10 +16,14 @@ type Spec struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
func (s *Spec) SpecDefinitionName() string {
|
||||
return Name
|
||||
}
|
||||
|
||||
func (s *Spec) SpecDefinitionVersion() string {
|
||||
return Version
|
||||
}
|
||||
|
||||
func (s *Spec) SpecRevision() int {
|
||||
return s.Revision
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore/memory"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -27,11 +28,15 @@ var validatorTestCases = []validatorTestCase{
|
||||
func TestValidator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validator := spec.NewValidator()
|
||||
if err := validator.Register(Name, schema); err != nil {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := memory.NewSpecDefinitionRepository()
|
||||
if _, err := repo.Upsert(ctx, Name, Version, schema); err != nil {
|
||||
t.Fatalf("+%v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
validator := spec.NewValidator(repo)
|
||||
|
||||
for _, tc := range validatorTestCases {
|
||||
func(tc validatorTestCase) {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
|
@ -31,7 +31,7 @@ func (*SysUpgradeController) Name() string {
|
||||
func (c *SysUpgradeController) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
sysSpec := sysupgrade.NewSpec()
|
||||
|
||||
if err := state.GetSpec(sysupgrade.Name, sysSpec); err != nil {
|
||||
if err := state.GetSpec(sysupgrade.Name, sysupgrade.Version, sysSpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find sysupgrade spec, doing nothing")
|
||||
|
||||
|
@ -27,7 +27,7 @@ func (*UCIController) Name() string {
|
||||
func (c *UCIController) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
uciSpec := ucispec.NewSpec()
|
||||
|
||||
if err := state.GetSpec(ucispec.NameUCI, uciSpec); err != nil {
|
||||
if err := state.GetSpec(ucispec.Name, ucispec.Version, uciSpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find uci spec, doing nothing")
|
||||
|
||||
@ -37,7 +37,11 @@ func (c *UCIController) Reconcile(ctx context.Context, state *agent.State) error
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "retrieved spec", logger.F("spec", uciSpec.SpecName()), logger.F("revision", uciSpec.SpecRevision()))
|
||||
logger.Info(ctx, "retrieved spec",
|
||||
logger.F("name", uciSpec.SpecDefinitionName()),
|
||||
logger.F("version", uciSpec.SpecDefinitionVersion()),
|
||||
logger.F("revision", uciSpec.SpecRevision()),
|
||||
)
|
||||
|
||||
if c.currentSpecRevision == uciSpec.SpecRevision() {
|
||||
logger.Info(ctx, "spec revision did not change, doing nothing")
|
||||
|
@ -9,13 +9,12 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
trackedSpecRevisions map[spec.Name]int
|
||||
trackedSpecRevisions map[string]map[string]int
|
||||
filename string
|
||||
loaded bool
|
||||
}
|
||||
@ -78,36 +77,39 @@ func (c *Controller) specChanged(specs agent.Specs) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
for name, spec := range specs {
|
||||
trackedRevision, exists := c.trackedSpecRevisions[name]
|
||||
for name, specVersions := range specs {
|
||||
trackedSpecs, exists := c.trackedSpecRevisions[name]
|
||||
if !exists {
|
||||
return true
|
||||
}
|
||||
|
||||
if trackedRevision != spec.SpecRevision() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for version, spec := range specVersions {
|
||||
trackedRevision, exists := trackedSpecs[version]
|
||||
if !exists {
|
||||
return true
|
||||
}
|
||||
|
||||
for trackedSpecName, trackedRevision := range c.trackedSpecRevisions {
|
||||
spec, exists := specs[trackedSpecName]
|
||||
if !exists {
|
||||
return true
|
||||
if trackedRevision != spec.SpecRevision() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if trackedRevision != spec.SpecRevision() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Controller) trackSpecsRevisions(specs agent.Specs) {
|
||||
c.trackedSpecRevisions = make(map[spec.Name]int)
|
||||
c.trackedSpecRevisions = make(map[string]map[string]int)
|
||||
|
||||
for name, spec := range specs {
|
||||
c.trackedSpecRevisions[name] = spec.SpecRevision()
|
||||
for name, specVersions := range specs {
|
||||
if _, exists := c.trackedSpecRevisions[name]; !exists {
|
||||
c.trackedSpecRevisions[name] = make(map[string]int)
|
||||
}
|
||||
|
||||
for version, spec := range specVersions {
|
||||
c.trackedSpecRevisions[name][version] = spec.SpecRevision()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,7 +169,7 @@ func (c *Controller) writeState(ctx context.Context, state *agent.State) error {
|
||||
}
|
||||
|
||||
name := f.Name()
|
||||
if err := ioutil.WriteFile(name, data, os.ModePerm); err != nil {
|
||||
if err := os.WriteFile(name, data, os.ModePerm); err != nil {
|
||||
return errors.Errorf("cannot write data to temporary file %q: %v", name, err)
|
||||
}
|
||||
|
||||
@ -213,7 +215,7 @@ func (c *Controller) writeState(ctx context.Context, state *agent.State) error {
|
||||
func NewController(filename string) *Controller {
|
||||
return &Controller{
|
||||
filename: filename,
|
||||
trackedSpecRevisions: make(map[spec.Name]int),
|
||||
trackedSpecRevisions: make(map[string]map[string]int),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ func (c *Controller) Name() string {
|
||||
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
proxySpec := spec.NewSpec()
|
||||
|
||||
if err := state.GetSpec(spec.NameProxy, proxySpec); err != nil {
|
||||
if err := state.GetSpec(spec.Name, spec.Version, proxySpec); err != nil {
|
||||
if errors.Is(err, agent.ErrSpecNotFound) {
|
||||
logger.Info(ctx, "could not find proxy spec")
|
||||
|
||||
@ -42,7 +42,12 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
logger.Info(ctx, "retrieved spec", logger.F("spec", proxySpec.SpecName()), logger.F("revision", proxySpec.SpecRevision()))
|
||||
logger.Info(
|
||||
ctx, "retrieved spec",
|
||||
logger.F("name", proxySpec.SpecDefinitionName()),
|
||||
logger.F("version", proxySpec.SpecDefinitionVersion()),
|
||||
logger.F("revision", proxySpec.SpecRevision()),
|
||||
)
|
||||
|
||||
c.updateProxies(ctx, proxySpec)
|
||||
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
|
||||
var ErrSpecNotFound = errors.New("spec not found")
|
||||
|
||||
type Specs map[spec.Name]spec.Spec
|
||||
type Specs map[string]map[string]spec.Spec
|
||||
|
||||
type State struct {
|
||||
agentID datastore.AgentID
|
||||
@ -20,25 +20,33 @@ type State struct {
|
||||
|
||||
func NewState() *State {
|
||||
return &State{
|
||||
specs: make(map[spec.Name]spec.Spec),
|
||||
specs: make(map[string]map[string]spec.Spec),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) MarshalJSON() ([]byte, error) {
|
||||
state := struct {
|
||||
ID datastore.AgentID `json:"agentId"`
|
||||
Specs map[spec.Name]*spec.RawSpec `json:"specs"`
|
||||
ID datastore.AgentID `json:"agentId"`
|
||||
Specs map[string]map[string]*spec.RawSpec `json:"specs"`
|
||||
}{
|
||||
ID: s.agentID,
|
||||
Specs: func(specs map[spec.Name]spec.Spec) map[spec.Name]*spec.RawSpec {
|
||||
rawSpecs := make(map[spec.Name]*spec.RawSpec)
|
||||
Specs: func(specs map[string]map[string]spec.Spec) map[string]map[string]*spec.RawSpec {
|
||||
rawSpecs := make(map[string]map[string]*spec.RawSpec)
|
||||
|
||||
for name, sp := range specs {
|
||||
rawSpecs[name] = &spec.RawSpec{
|
||||
Name: sp.SpecName(),
|
||||
Revision: sp.SpecRevision(),
|
||||
Data: sp.SpecData(),
|
||||
for name, versions := range specs {
|
||||
if _, exists := rawSpecs[name]; !exists {
|
||||
rawSpecs[name] = make(map[string]*spec.RawSpec)
|
||||
}
|
||||
|
||||
for version, sp := range versions {
|
||||
rawSpecs[name][version] = &spec.RawSpec{
|
||||
DefinitionName: sp.SpecDefinitionName(),
|
||||
DefinitionVersion: sp.SpecDefinitionVersion(),
|
||||
Revision: sp.SpecRevision(),
|
||||
Data: sp.SpecData(),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return rawSpecs
|
||||
@ -55,19 +63,24 @@ func (s *State) MarshalJSON() ([]byte, error) {
|
||||
|
||||
func (s *State) UnmarshalJSON(data []byte) error {
|
||||
state := struct {
|
||||
AgentID datastore.AgentID `json:"agentId"`
|
||||
Specs map[spec.Name]*spec.RawSpec `json:"specs"`
|
||||
AgentID datastore.AgentID `json:"agentId"`
|
||||
Specs map[string]map[string]*spec.RawSpec `json:"specs"`
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
s.specs = func(rawSpecs map[spec.Name]*spec.RawSpec) Specs {
|
||||
s.specs = func(rawSpecs map[string]map[string]*spec.RawSpec) Specs {
|
||||
specs := make(Specs)
|
||||
|
||||
for name, raw := range rawSpecs {
|
||||
specs[name] = spec.Spec(raw)
|
||||
for name, versions := range rawSpecs {
|
||||
if _, exists := specs[name]; !exists {
|
||||
specs[name] = make(map[string]spec.Spec)
|
||||
}
|
||||
for version, raw := range versions {
|
||||
specs[name][version] = spec.Spec(raw)
|
||||
}
|
||||
}
|
||||
|
||||
return specs
|
||||
@ -85,23 +98,36 @@ func (s *State) Specs() Specs {
|
||||
}
|
||||
|
||||
func (s *State) ClearSpecs() *State {
|
||||
s.specs = make(map[spec.Name]spec.Spec)
|
||||
s.specs = make(map[string]map[string]spec.Spec)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *State) SetSpec(sp spec.Spec) *State {
|
||||
if s.specs == nil {
|
||||
s.specs = make(map[spec.Name]spec.Spec)
|
||||
s.specs = make(map[string]map[string]spec.Spec)
|
||||
}
|
||||
|
||||
s.specs[sp.SpecName()] = sp
|
||||
name := sp.SpecDefinitionName()
|
||||
|
||||
if _, exists := s.specs[name]; !exists {
|
||||
s.specs[name] = make(map[string]spec.Spec)
|
||||
}
|
||||
|
||||
version := sp.SpecDefinitionVersion()
|
||||
|
||||
s.specs[name][version] = sp
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *State) GetSpec(name spec.Name, dest any) error {
|
||||
spec, exists := s.specs[name]
|
||||
func (s *State) GetSpec(name string, version string, dest any) error {
|
||||
versions, exists := s.specs[name]
|
||||
if !exists {
|
||||
return errors.WithStack(ErrSpecNotFound)
|
||||
}
|
||||
|
||||
spec, exists := versions[version]
|
||||
if !exists {
|
||||
return errors.WithStack(ErrSpecNotFound)
|
||||
}
|
||||
|
Reference in New Issue
Block a user