Skip to content
Open
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
2 changes: 0 additions & 2 deletions collocated.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ grpc-server:
# username: admin
# password: admin
# sync:
# validate: true
# config:
# - name: config
# protocol: netconf
Expand All @@ -81,7 +80,6 @@ grpc-server:
# tls:
# skip-verify: true
# sync:
# validate: true
# gnmi:
# - path:
# - /
Expand Down
2 changes: 0 additions & 2 deletions data-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ datastores: # this specifies MAIN datastores
# tls:
# skip-verify: true
# sync:
# validate: true
# gnmi:
# - path:
# - /
Expand All @@ -81,7 +80,6 @@ datastores: # this specifies MAIN datastores
# tls:
# skip-verify: true
# sync:
# validate: true
# gnmi:
# - path:
# - /
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,5 @@ require (
sigs.k8s.io/structured-merge-diff/v4 v4.4.3 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

replace github.com/sdcio/sdc-protos => github.com/sdcio/sdc-protos v0.0.57-0.20261109153201-334b5937c4ae
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,8 @@ github.com/sdcio/logger v0.0.3 h1:IFUbObObGry+S8lHGwOQKKRxJSuOphgRU/hxVhOdMOM=
github.com/sdcio/logger v0.0.3/go.mod h1:yWaOxK/G6vszjg8tKZiMqiEjlZouHsjFME4zSk+SAEA=
github.com/sdcio/schema-server v0.0.34 h1:NNDOkvtUMONtBA7cVvN96F+FWGD/Do6HNqfchy9B8eI=
github.com/sdcio/schema-server v0.0.34/go.mod h1:6t8HLXpqUqEJmE5yNZh29u/KZw0jlOICdNWns7zE4GE=
github.com/sdcio/sdc-protos v0.0.56 h1:8tEbo53/2KAd/5mruqiKhNsDljKcWI6nhCfriYE2SvE=
github.com/sdcio/sdc-protos v0.0.56/go.mod h1:hER0riYU3G55NnMQ0p0R9kIL/fzqq1zab4pOgUT3Yfg=
github.com/sdcio/sdc-protos v0.0.57-0.20261109153201-334b5937c4ae h1:6vIatz59rXaWl5HVrJWrvOxfBCLjhJBnKeOEc5ischE=
github.com/sdcio/sdc-protos v0.0.57-0.20261109153201-334b5937c4ae/go.mod h1:hER0riYU3G55NnMQ0p0R9kIL/fzqq1zab4pOgUT3Yfg=
github.com/sdcio/yang-parser v0.0.12 h1:RSSeqfAOIsJx5Lno5u4/ezyOmQYUduQ22rBfU/mtpJ4=
github.com/sdcio/yang-parser v0.0.12/go.mod h1:CBqn3Miq85qmFVGHxHXHLluXkaIOsTzV06IM4DW6+D4=
github.com/sirikothe/gotextfsm v1.0.1-0.20200816110946-6aa2cfd355e4 h1:FHUL2HofYJuslFOQdy/JjjP36zxqIpd/dcoiwLMIs7k=
Expand Down
5 changes: 0 additions & 5 deletions lab/distributed/data-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ datastores: # this creates MAIN datastores
tls:
skip-verify: true
sync:
validate: false
gnmi:
- name: config
paths:
Expand Down Expand Up @@ -67,7 +66,6 @@ datastores: # this creates MAIN datastores
tls:
skip-verify: true
sync:
validate: false
gnmi:
- name: config
paths:
Expand All @@ -94,7 +92,6 @@ datastores: # this creates MAIN datastores
tls:
skip-verify: true
sync:
validate: false
gnmi:
- name: config
paths:
Expand All @@ -121,7 +118,6 @@ datastores: # this creates MAIN datastores
password: admin
# include-ns: true
sync:
validate: false
gnmi:
- name: config
paths:
Expand Down Expand Up @@ -149,7 +145,6 @@ datastores: # this creates MAIN datastores
password: admin
# include-ns: true
sync:
validate: false
gnmi:
- name: config
paths:
Expand Down
149 changes: 149 additions & 0 deletions pkg/api/datastore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package api

import (
"fmt"
"strings"
)

// TargetType names the southbound protocol of a datastore target.
type TargetType string

const (
TargetTypeGNMI TargetType = "gnmi"
TargetTypeNetconf TargetType = "netconf"
)

// ConnectionState reports the southbound connection state of a datastore.
type ConnectionState int

const (
ConnectionUnknown ConnectionState = iota
ConnectionConnected
ConnectionNotConnected
)

// Credentials authenticate against a target.
type Credentials struct {
Username string
Password string
Token string
}

// TLS configures the client side of the target connection.
type TLS struct {
CA string
Cert string
Key string
SkipVerify bool
}

// CommitCandidate selects which NETCONF datastore is committed.
type CommitCandidate int

const (
CommitCandidateCandidate CommitCandidate = iota
CommitCandidateRunning
)

// GNMIEncoding names a gNMI value encoding, spelled as in the gNMI spec.
type GNMIEncoding string

const (
GNMIEncodingJSON GNMIEncoding = "JSON"
GNMIEncodingBytes GNMIEncoding = "BYTES"
GNMIEncodingProto GNMIEncoding = "PROTO"
GNMIEncodingASCII GNMIEncoding = "ASCII"
GNMIEncodingJSONIETF GNMIEncoding = "JSON_IETF"
// GNMIEncodingConfig is the vendor-specific CONFIG encoding, sent as gNMI encoding 45.
GNMIEncodingConfig GNMIEncoding = "CONFIG"
)

// ParseGNMIEncoding resolves a case-insensitive encoding name. Unknown names,
// including numeric values, are rejected.
func ParseGNMIEncoding(s string) (GNMIEncoding, error) {
switch e := GNMIEncoding(strings.ToUpper(s)); e {
case GNMIEncodingJSON, GNMIEncodingBytes, GNMIEncodingProto, GNMIEncodingASCII, GNMIEncodingJSONIETF, GNMIEncodingConfig:
return e, nil
}
return "", fmt.Errorf("unknown gnmi encoding %q", s)
}

// GNMIOptions are the gNMI-specific protocol options.
type GNMIOptions struct {
Encoding GNMIEncoding
TargetName string
}

// NetconfOptions are the NETCONF-specific protocol options.
type NetconfOptions struct {
IncludeNs bool
OperationWithNs bool
UseOperationRemove bool
CommitCandidate CommitCandidate
}

// ProtocolOptions is a union: at most one of GNMI and Netconf is set.
type ProtocolOptions struct {
GNMI *GNMIOptions
Netconf *NetconfOptions
}

// Target describes how to reach a device.
type Target struct {
Type TargetType
Address string
Port uint32
Credentials *Credentials
TLS *TLS
ProtocolOptions ProtocolOptions
}

// SchemaRef identifies a schema by vendor and version.
type SchemaRef struct {
Vendor string
Version string
}

// SyncMode selects how a sync strategy collects device state.
type SyncMode int

const (
SyncModeOnChange SyncMode = iota
SyncModeSample
SyncModeOnce
SyncModeGet
)

// SyncConfig is one sync strategy. Target, when set, overrides the datastore
// target for this strategy.
type SyncConfig struct {
Name string
Target *Target
Paths []string
Mode SyncMode
Interval uint64
}

// Sync is the datastore sync strategy.
type Sync struct {
Configs []SyncConfig
}

// DatastoreSpec is the desired state used to create a datastore.
type DatastoreSpec struct {
Name string
Target Target
Schema SchemaRef
Sync Sync
}

// DatastoreInfo is the observed state of a datastore.
type DatastoreInfo struct {
Name string
ConnectionState ConnectionState
// StatusDetails explains a not-connected state when available.
StatusDetails string
Intents []string
// Spec is the effective spec the datastore runs with.
Spec DatastoreSpec
}
16 changes: 16 additions & 0 deletions pkg/api/datastore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package api

import "testing"

func TestParseGNMIEncoding(t *testing.T) {
for in, want := range map[string]GNMIEncoding{"json_ietf": GNMIEncodingJSONIETF, "JSON": GNMIEncodingJSON, "Proto": GNMIEncodingProto, "config": GNMIEncodingConfig} {
if got, err := ParseGNMIEncoding(in); err != nil || got != want {
t.Errorf("%q: got %q, %v", in, got, err)
}
}
for _, in := range []string{"", "jsno", "4"} {
if _, err := ParseGNMIEncoding(in); err == nil {
t.Errorf("%q accepted", in)
}
}
}
41 changes: 41 additions & 0 deletions pkg/api/deviation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package api

import sdcpb "github.com/sdcio/sdc-protos/sdcpb"

// DeviationKind classifies a DeviationEvent: Start opens a deviation stream
// for a datastore, Update reports a changed deviation, End closes the stream
// normally and Clear drops all deviations previously reported for the intent.
type DeviationKind int

const (
DeviationStart DeviationKind = iota
DeviationUpdate
DeviationEnd
DeviationClear
)

// DeviationReason explains why a deviation exists.
type DeviationReason int

const (
DeviationReasonUnknown DeviationReason = iota
DeviationReasonUnhandled
DeviationReasonNotApplied
DeviationReasonOverruled
// DeviationReasonIntentExists is not a deviation reason: it marks that the
// named intent exists, and is sent without path or values.
DeviationReasonIntentExists
)

// DeviationEvent is emitted by a deviation watch, one per deviating path.
// Intent, Reason, Path, Expected and Current are set only where the Kind
// carries them.
type DeviationEvent struct {
Datastore string
Kind DeviationKind
Intent string
Reason DeviationReason
Path *sdcpb.Path
Expected *sdcpb.TypedValue
Current *sdcpb.TypedValue
}
6 changes: 6 additions & 0 deletions pkg/api/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Package api holds the domain types, error sentinels and helpers shared by the
// data-server core and its consumers. It is types-only: it imports neither gRPC
// nor any data-server internals. The only sdcpb types it uses are TypedValue,
// Path and Update, the value, address and path-value types of the domain; no
// other sdcpb type may appear here.
package api
36 changes: 36 additions & 0 deletions pkg/api/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package api

import (
"context"
"errors"
)

// Sentinels are matched with errors.Is; implementations wrap them with context.
var (
// ErrDatastoreLocked: another action holds the datastore; retry later.
ErrDatastoreLocked = errors.New("datastore is locked, other action is ongoing")
// ErrNotConnected: the southbound target is unreachable; retry later.
ErrNotConnected = errors.New("not connected")
// ErrResourceExhausted: a resource limit was hit; retry later.
ErrResourceExhausted = errors.New("resource exhausted")
// ErrUnknownDatastore: the named datastore does not exist.
ErrUnknownDatastore = errors.New("unknown datastore")
// ErrInvalidArgument: the request is malformed.
ErrInvalidArgument = errors.New("invalid argument")
)

// IsRetryable reports whether err is transient, so the same request may
// succeed later. Untyped errors are not retryable.
func IsRetryable(err error) bool {
switch {
case err == nil:
return false
case errors.Is(err, ErrDatastoreLocked),
errors.Is(err, ErrNotConnected),
errors.Is(err, ErrResourceExhausted),
errors.Is(err, context.DeadlineExceeded):
return true
default:
return false
}
}
56 changes: 56 additions & 0 deletions pkg/api/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package api

import (
"context"
"errors"
"fmt"
"testing"
)

func TestIsRetryable(t *testing.T) {
tests := []struct {
name string
err error
want bool
}{
{"nil", nil, false},
{"locked", ErrDatastoreLocked, true},
{"not connected", ErrNotConnected, true},
{"resource exhausted", ErrResourceExhausted, true},
{"deadline", context.DeadlineExceeded, true},
{"wrapped locked", fmt.Errorf("ds1: %w", ErrDatastoreLocked), true},
{"joined not connected", errors.Join(errors.New("x"), fmt.Errorf("a: %w", ErrNotConnected)), true},
{"unknown datastore", ErrUnknownDatastore, false},
{"invalid argument", ErrInvalidArgument, false},
{"canceled", context.Canceled, false},
{"untyped", errors.New("boom"), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsRetryable(tt.err); got != tt.want {
t.Fatalf("IsRetryable(%v) = %v, want %v", tt.err, got, tt.want)
}
})
}
}

func TestSentinelsAreDistinct(t *testing.T) {
all := []error{ErrDatastoreLocked, ErrNotConnected, ErrResourceExhausted, ErrUnknownDatastore, ErrInvalidArgument}
for i, a := range all {
for j, b := range all {
if (i == j) != errors.Is(a, b) {
t.Fatalf("errors.Is(%v, %v) = %v", a, b, errors.Is(a, b))
}
}
}
}

func TestErrorsIsThroughWrapping(t *testing.T) {
err := fmt.Errorf("outer: %w", fmt.Errorf("inner: %w", ErrUnknownDatastore))
if !errors.Is(err, ErrUnknownDatastore) {
t.Fatal("wrapped sentinel not matched")
}
if errors.Is(err, ErrInvalidArgument) {
t.Fatal("unrelated sentinel matched")
}
}
Loading
Loading