fix: write etcd PKI files in a controller

Instead of writing PKI "once" around the startup time, keep writing PKI
files as the certificates get updated. `etcd` is able to reload
certificates, so we should keep updating them e.g. if the hostname/IPs
change over time.

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
This commit is contained in:
Andrey Smirnov 2022-07-21 18:37:45 +04:00
parent bb4abc0961
commit a2aea97263
No known key found for this signature in database
GPG Key ID: 7B26396447AB6DFD
17 changed files with 345 additions and 136 deletions

View File

@ -253,8 +253,7 @@ COPY --from=generate-build /api/storage/*.pb.go /pkg/machinery/api/storage/
COPY --from=generate-build /api/resource/*.pb.go /pkg/machinery/api/resource/
COPY --from=generate-build /api/resource/secrets/*.pb.go /pkg/machinery/api/resource/secrets/
COPY --from=generate-build /api/inspect/*.pb.go /pkg/machinery/api/inspect/
COPY --from=go-generate /src/pkg/machinery/resources/kubespan/ /pkg/machinery/resources/kubespan/
COPY --from=go-generate /src/pkg/machinery/resources/network/ /pkg/machinery/resources/network/
COPY --from=go-generate /src/pkg/machinery/resources/ /pkg/machinery/resources/
COPY --from=go-generate /src/pkg/machinery/config/types/v1alpha1/ /pkg/machinery/config/types/v1alpha1/
COPY --from=go-generate /src/pkg/machinery/nethelpers/ /pkg/machinery/nethelpers/
COPY --from=go-generate /src/pkg/machinery/extensions/ /pkg/machinery/extensions/

View File

@ -0,0 +1,6 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package etcd provides controllers which manage etcd resources.
package etcd

View File

@ -0,0 +1,148 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package etcd
import (
"context"
"fmt"
"os"
"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/safe"
"github.com/cosi-project/runtime/pkg/state"
"github.com/siderolabs/go-pointer"
"github.com/talos-systems/crypto/x509"
"go.uber.org/zap"
"github.com/talos-systems/talos/pkg/filetree"
"github.com/talos-systems/talos/pkg/machinery/constants"
"github.com/talos-systems/talos/pkg/machinery/resources/etcd"
"github.com/talos-systems/talos/pkg/machinery/resources/secrets"
)
// PKIController renders manifests based on templates and config/secrets.
type PKIController struct{}
// Name implements controller.Controller interface.
func (ctrl *PKIController) Name() string {
return "etcd.PKIController"
}
// Inputs implements controller.Controller interface.
func (ctrl *PKIController) Inputs() []controller.Input {
return []controller.Input{
{
Namespace: secrets.NamespaceName,
Type: secrets.EtcdRootType,
ID: pointer.To(secrets.EtcdRootID),
Kind: controller.InputWeak,
},
{
Namespace: secrets.NamespaceName,
Type: secrets.EtcdType,
ID: pointer.To(secrets.EtcdID),
Kind: controller.InputWeak,
},
}
}
// Outputs implements controller.Controller interface.
func (ctrl *PKIController) Outputs() []controller.Output {
return []controller.Output{
{
Type: etcd.PKIStatusType,
Kind: controller.OutputExclusive,
},
}
}
// Run implements controller.Controller interface.
//
//nolint:gocyclo
func (ctrl *PKIController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
for {
select {
case <-ctx.Done():
return nil
case <-r.EventCh():
}
rootScrts, err := safe.ReaderGet[*secrets.EtcdRoot](ctx, r, resource.NewMetadata(secrets.NamespaceName, secrets.EtcdRootType, secrets.EtcdRootID, resource.VersionUndefined))
if err != nil {
if state.IsNotFoundError(err) {
continue
}
return fmt.Errorf("error getting root secrets: %w", err)
}
scrts, err := safe.ReaderGet[*secrets.Etcd](ctx, r, resource.NewMetadata(secrets.NamespaceName, secrets.EtcdType, secrets.EtcdID, resource.VersionUndefined))
if err != nil {
if state.IsNotFoundError(err) {
continue
}
return fmt.Errorf("error getting secrets: %w", err)
}
if err = os.MkdirAll(constants.EtcdPKIPath, 0o700); err != nil {
return err
}
if err = os.WriteFile(constants.KubernetesEtcdCACert, rootScrts.TypedSpec().EtcdCA.Crt, 0o400); err != nil {
return fmt.Errorf("failed to write CA certificate: %w", err)
}
if err = os.WriteFile(constants.KubernetesEtcdCAKey, rootScrts.TypedSpec().EtcdCA.Key, 0o400); err != nil {
return fmt.Errorf("failed to write CA key: %w", err)
}
etcdCerts := scrts.TypedSpec()
for _, keypair := range []struct {
getter func() *x509.PEMEncodedCertificateAndKey
keyPath string
certPath string
}{
{
getter: func() *x509.PEMEncodedCertificateAndKey { return etcdCerts.Etcd },
keyPath: constants.KubernetesEtcdKey,
certPath: constants.KubernetesEtcdCert,
},
{
getter: func() *x509.PEMEncodedCertificateAndKey { return etcdCerts.EtcdPeer },
keyPath: constants.KubernetesEtcdPeerKey,
certPath: constants.KubernetesEtcdPeerCert,
},
{
getter: func() *x509.PEMEncodedCertificateAndKey { return etcdCerts.EtcdAdmin },
keyPath: constants.KubernetesEtcdAdminKey,
certPath: constants.KubernetesEtcdAdminCert,
},
} {
if err = os.WriteFile(keypair.keyPath, keypair.getter().Key, 0o400); err != nil {
return err
}
if err = os.WriteFile(keypair.certPath, keypair.getter().Crt, 0o400); err != nil {
return err
}
}
if err = filetree.ChownRecursive(constants.EtcdPKIPath, constants.EtcdUserID, constants.EtcdUserID); err != nil {
return nil
}
if err = safe.WriterModify(ctx, r, etcd.NewPKIStatus(etcd.NamespaceName, etcd.PKIID), func(status *etcd.PKIStatus) error {
status.TypedSpec().Ready = true
status.TypedSpec().Version = scrts.Metadata().Version().String()
return nil
}); err != nil {
return fmt.Errorf("error updating PKI status: %w", err)
}
}
}

View File

@ -21,6 +21,7 @@ import (
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/cluster"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/config"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/etcd"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/files"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/hardware"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/k8s"
@ -103,6 +104,7 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error
&config.MachineTypeController{},
&config.K8sAddressFilterController{},
&config.K8sControlPlaneController{},
&etcd.PKIController{},
&files.CRIConfigPartsController{},
&files.CRIRegistryConfigController{},
&files.EtcFileController{

View File

@ -16,6 +16,7 @@ import (
talosconfig "github.com/talos-systems/talos/pkg/machinery/config"
"github.com/talos-systems/talos/pkg/machinery/resources/cluster"
"github.com/talos-systems/talos/pkg/machinery/resources/config"
"github.com/talos-systems/talos/pkg/machinery/resources/etcd"
"github.com/talos-systems/talos/pkg/machinery/resources/files"
"github.com/talos-systems/talos/pkg/machinery/resources/hardware"
"github.com/talos-systems/talos/pkg/machinery/resources/k8s"
@ -63,6 +64,7 @@ func NewState() (*State, error) {
{cluster.NamespaceName, "Cluster configuration and discovery resources."},
{cluster.RawNamespaceName, "Cluster unmerged raw resources."},
{config.NamespaceName, "Talos node configuration."},
{etcd.NamespaceName, "etcd resources."},
{files.NamespaceName, "Files and file-like resources."},
{hardware.NamespaceName, "Hardware resources."},
{k8s.NamespaceName, "Kubernetes all node types resources."},
@ -87,6 +89,7 @@ func NewState() (*State, error) {
&cluster.Member{},
&config.MachineConfig{},
&config.MachineType{},
&etcd.PKIStatus{},
&files.EtcFileSpec{},
&files.EtcFileStatus{},
&hardware.Processor{},

View File

@ -9,7 +9,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
goruntime "runtime"
@ -23,7 +22,6 @@ import (
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/talos-systems/crypto/x509"
"github.com/talos-systems/go-retry/retry"
"github.com/talos-systems/net"
clientv3 "go.etcd.io/etcd/client/v3"
@ -42,13 +40,14 @@ import (
"github.com/talos-systems/talos/internal/pkg/etcd"
"github.com/talos-systems/talos/pkg/argsbuilder"
"github.com/talos-systems/talos/pkg/conditions"
"github.com/talos-systems/talos/pkg/filetree"
"github.com/talos-systems/talos/pkg/logging"
machineapi "github.com/talos-systems/talos/pkg/machinery/api/machine"
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine"
"github.com/talos-systems/talos/pkg/machinery/constants"
etcdresource "github.com/talos-systems/talos/pkg/machinery/resources/etcd"
"github.com/talos-systems/talos/pkg/machinery/resources/k8s"
"github.com/talos-systems/talos/pkg/machinery/resources/network"
"github.com/talos-systems/talos/pkg/machinery/resources/secrets"
timeresource "github.com/talos-systems/talos/pkg/machinery/resources/time"
)
@ -87,7 +86,7 @@ func (e *Etcd) PreFunc(ctx context.Context, r runtime.Runtime) (err error) {
}
// Make sure etcd user can access files in the data directory.
if err = chownRecursive(constants.EtcdDataPath, constants.EtcdUserID, constants.EtcdUserID); err != nil {
if err = filetree.ChownRecursive(constants.EtcdDataPath, constants.EtcdUserID, constants.EtcdUserID); err != nil {
return err
}
@ -126,7 +125,7 @@ func (e *Etcd) PreFunc(ctx context.Context, r runtime.Runtime) (err error) {
panic(fmt.Sprintf("unexpected machine type %v", t))
}
if err = generatePKI(ctx, r); err != nil {
if err = waitPKI(ctx, r); err != nil {
return fmt.Errorf("failed to generate etcd PKI: %w", err)
}
@ -243,102 +242,16 @@ func (e *Etcd) HealthSettings(runtime.Runtime) *health.Settings {
}
}
//nolint:gocyclo
func generatePKI(ctx context.Context, r runtime.Runtime) (err error) {
if err = os.MkdirAll(constants.EtcdPKIPath, 0o700); err != nil {
return err
}
func waitPKI(ctx context.Context, r runtime.Runtime) error {
_, err := r.State().V1Alpha2().Resources().WatchFor(ctx,
resource.NewMetadata(etcdresource.NamespaceName, etcdresource.PKIStatusType, etcdresource.PKIID, resource.VersionUndefined),
state.WithEventTypes(state.Created, state.Updated),
)
if err = ioutil.WriteFile(constants.KubernetesEtcdCACert, r.Config().Cluster().Etcd().CA().Crt, 0o400); err != nil {
return fmt.Errorf("failed to write CA certificate: %w", err)
}
if err = ioutil.WriteFile(constants.KubernetesEtcdCAKey, r.Config().Cluster().Etcd().CA().Key, 0o400); err != nil {
return fmt.Errorf("failed to write CA key: %w", err)
}
// wait for etcd certificates to be generated in the controller
watchCh := make(chan state.Event)
if err = r.State().V1Alpha2().Resources().Watch(ctx, resource.NewMetadata(secrets.NamespaceName, secrets.EtcdType, secrets.EtcdID, resource.VersionUndefined), watchCh); err != nil {
return err
}
var event state.Event
for {
select {
case <-ctx.Done():
return ctx.Err()
case event = <-watchCh:
}
if event.Type == state.Created || event.Type == state.Updated {
break
}
}
// wait for additional events with 500ms timeout and absorb new versions of the resource
const settleDownTimeout = 500 * time.Millisecond
timer := time.NewTimer(settleDownTimeout)
defer timer.Stop()
waitLoop:
for {
select {
case event = <-watchCh:
if !timer.Stop() {
<-timer.C
}
timer.Reset(settleDownTimeout)
case <-timer.C:
break waitLoop
}
}
etcdCerts := event.Resource.(*secrets.Etcd).TypedSpec()
for _, keypair := range []struct {
getter func() *x509.PEMEncodedCertificateAndKey
keyPath string
certPath string
}{
{
getter: func() *x509.PEMEncodedCertificateAndKey { return etcdCerts.Etcd },
keyPath: constants.KubernetesEtcdKey,
certPath: constants.KubernetesEtcdCert,
},
{
getter: func() *x509.PEMEncodedCertificateAndKey { return etcdCerts.EtcdPeer },
keyPath: constants.KubernetesEtcdPeerKey,
certPath: constants.KubernetesEtcdPeerCert,
},
{
getter: func() *x509.PEMEncodedCertificateAndKey { return etcdCerts.EtcdAdmin },
keyPath: constants.KubernetesEtcdAdminKey,
certPath: constants.KubernetesEtcdAdminCert,
},
} {
if err = ioutil.WriteFile(keypair.keyPath, keypair.getter().Key, 0o400); err != nil {
return err
}
if err = ioutil.WriteFile(keypair.certPath, keypair.getter().Crt, 0o400); err != nil {
return err
}
}
return chownRecursive(constants.EtcdPKIPath, constants.EtcdUserID, constants.EtcdUserID)
return err
}
func addMember(ctx context.Context, r runtime.Runtime, addrs []string, name string) (*clientv3.MemberListResponse, uint64, error) {
// update PKI on each join attempt
if err := generatePKI(ctx, r); err != nil {
return nil, 0, fmt.Errorf("failed to generate etcd PKI: %w", err)
}
client, err := etcd.NewClientFromControlPlaneIPsNoDiscovery(ctx, r.State().V1Alpha2().Resources())
if err != nil {
return nil, 0, err
@ -672,7 +585,7 @@ func (e *Etcd) recoverFromSnapshot(hostname, primaryAddr string) error {
return fmt.Errorf("error deleting snapshot: %w", err)
}
return chownRecursive(constants.EtcdDataPath, constants.EtcdUserID, constants.EtcdUserID)
return filetree.ChownRecursive(constants.EtcdDataPath, constants.EtcdUserID, constants.EtcdUserID)
}
func promoteMember(ctx context.Context, r runtime.Runtime, memberID uint64) error {

View File

@ -6,11 +6,9 @@ package services
import (
"fmt"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"syscall"
"golang.org/x/sys/unix"
@ -37,18 +35,3 @@ func prepareRootfs(id string) error {
return nil
}
// chownRecursive changes file ownership recursively from the specified root.
func chownRecursive(root string, uid, gid uint32) error {
return filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.Sys().(*syscall.Stat_t).Uid != uid || info.Sys().(*syscall.Stat_t).Gid != gid {
return os.Chown(path, int(uid), int(gid))
}
return nil
})
}

27
pkg/filetree/chown.go Normal file
View File

@ -0,0 +1,27 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package filetree
import (
"io/fs"
"os"
"path/filepath"
"syscall"
)
// ChownRecursive changes file ownership recursively from the specified root.
func ChownRecursive(root string, uid, gid uint32) error {
return filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if info.Sys().(*syscall.Stat_t).Uid != uid || info.Sys().(*syscall.Stat_t).Gid != gid {
return os.Chown(path, int(uid), int(gid))
}
return nil
})
}

6
pkg/filetree/filetree.go Normal file
View File

@ -0,0 +1,6 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package filetree provides file tree operations.
package filetree

View File

@ -2,7 +2,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Code generated by "deep-copy -type AffiliateSpec -type IdentitySpec -type MemberSpec -type ConfigSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT.
// Code generated by "deep-copy -type AffiliateSpec -type ConfigSpec -type IdentitySpec -type MemberSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT.
package cluster
@ -28,6 +28,16 @@ func (o AffiliateSpec) DeepCopy() AffiliateSpec {
return cp
}
// DeepCopy generates a deep copy of ConfigSpec.
func (o ConfigSpec) DeepCopy() ConfigSpec {
var cp ConfigSpec = o
if o.ServiceEncryptionKey != nil {
cp.ServiceEncryptionKey = make([]byte, len(o.ServiceEncryptionKey))
copy(cp.ServiceEncryptionKey, o.ServiceEncryptionKey)
}
return cp
}
// DeepCopy generates a deep copy of IdentitySpec.
func (o IdentitySpec) DeepCopy() IdentitySpec {
var cp IdentitySpec = o
@ -43,13 +53,3 @@ func (o MemberSpec) DeepCopy() MemberSpec {
}
return cp
}
// DeepCopy generates a deep copy of ConfigSpec.
func (o ConfigSpec) DeepCopy() ConfigSpec {
var cp ConfigSpec = o
if o.ServiceEncryptionKey != nil {
cp.ServiceEncryptionKey = make([]byte, len(o.ServiceEncryptionKey))
copy(cp.ServiceEncryptionKey, o.ServiceEncryptionKey)
}
return cp
}

View File

@ -0,0 +1,13 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Code generated by "deep-copy -type PKIStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT.
package etcd
// DeepCopy generates a deep copy of PKIStatusSpec.
func (o PKIStatusSpec) DeepCopy() PKIStatusSpec {
var cp PKIStatusSpec = o
return cp
}

View File

@ -0,0 +1,13 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package etcd provides resources which interface with etcd.
package etcd
import "github.com/cosi-project/runtime/pkg/resource"
//go:generate deep-copy -type PKIStatusSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go .
// NamespaceName contains resources supporting etcd service.
const NamespaceName resource.Namespace = "etcd"

View File

@ -0,0 +1,32 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package etcd_test
import (
"context"
"testing"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/cosi-project/runtime/pkg/state/registry"
"github.com/stretchr/testify/assert"
"github.com/talos-systems/talos/pkg/machinery/resources/etcd"
)
func TestRegisterResource(t *testing.T) {
ctx := context.TODO()
resources := state.WrapCore(namespaced.NewState(inmem.Build))
resourceRegistry := registry.NewResourceRegistry(resources)
for _, resource := range []resource.Resource{
&etcd.PKIStatus{},
} {
assert.NoError(t, resourceRegistry.Register(ctx, resource))
}
}

View File

@ -0,0 +1,56 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package etcd
import (
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/meta"
"github.com/cosi-project/runtime/pkg/resource/typed"
)
// PKIStatusType is type of PKIStatus resource.
const PKIStatusType = resource.Type("PKIStatuses.etcd.talos.dev")
// PKIID is resource ID for PKIStatus resource for etcd.
const PKIID = resource.ID("etcd")
// PKIStatus resource holds status of rendered secrets.
type PKIStatus = typed.Resource[PKIStatusSpec, PKIStatusRD]
// PKIStatusSpec describes status of rendered secrets.
type PKIStatusSpec struct {
Ready bool `yaml:"ready"`
Version string `yaml:"version"`
}
// NewPKIStatus initializes a PKIStatus resource.
func NewPKIStatus(namespace resource.Namespace, id resource.ID) *PKIStatus {
return typed.NewResource[PKIStatusSpec, PKIStatusRD](
resource.NewMetadata(namespace, PKIStatusType, id, resource.VersionUndefined),
PKIStatusSpec{},
)
}
// PKIStatusRD provides auxiliary methods for PKIStatus.
type PKIStatusRD struct{}
// ResourceDefinition implements typed.ResourceDefinition interface.
func (PKIStatusRD) ResourceDefinition(resource.Metadata, PKIStatusSpec) meta.ResourceDefinitionSpec {
return meta.ResourceDefinitionSpec{
Type: PKIStatusType,
Aliases: []resource.Type{},
DefaultNamespace: NamespaceName,
PrintColumns: []meta.PrintColumn{
{
Name: "Ready",
JSONPath: "{.ready}",
},
{
Name: "Secrets Version",
JSONPath: "{.version}",
},
},
}
}

View File

@ -2,22 +2,22 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Code generated by "deep-copy -type ProcessorSpec -type MemoryModuleSpec -type SystemInformationSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT.
// Code generated by "deep-copy -type MemoryModuleSpec -type ProcessorSpec -type SystemInformationSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go ."; DO NOT EDIT.
package hardware
// DeepCopy generates a deep copy of ProcessorSpec.
func (o ProcessorSpec) DeepCopy() ProcessorSpec {
var cp ProcessorSpec = o
return cp
}
// DeepCopy generates a deep copy of MemoryModuleSpec.
func (o MemoryModuleSpec) DeepCopy() MemoryModuleSpec {
var cp MemoryModuleSpec = o
return cp
}
// DeepCopy generates a deep copy of ProcessorSpec.
func (o ProcessorSpec) DeepCopy() ProcessorSpec {
var cp ProcessorSpec = o
return cp
}
// DeepCopy generates a deep copy of SystemInformationSpec.
func (o SystemInformationSpec) DeepCopy() SystemInformationSpec {
var cp SystemInformationSpec = o

View File

@ -8,7 +8,7 @@ import (
"github.com/cosi-project/runtime/pkg/resource"
)
//go:generate deep-copy -type ProcessorSpec -type MemoryModuleSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go .
//go:generate deep-copy -type MemoryModuleSpec -type ProcessorSpec -type SystemInformationSpec -header-file ../../../../hack/boilerplate.txt -o deep_copy.generated.go .
// NamespaceName contains resources related to hardware as a whole.
const NamespaceName resource.Namespace = "hardware"

View File

@ -112,6 +112,14 @@ func (o KubernetesRootSpec) DeepCopy() KubernetesRootSpec {
*cp.Endpoint.User = *o.Endpoint.User
}
}
if o.LocalEndpoint != nil {
cp.LocalEndpoint = new(url.URL)
*cp.LocalEndpoint = *o.LocalEndpoint
if o.LocalEndpoint.User != nil {
cp.LocalEndpoint.User = new(url.Userinfo)
*cp.LocalEndpoint.User = *o.LocalEndpoint.User
}
}
if o.CertSANs != nil {
cp.CertSANs = make([]string, len(o.CertSANs))
copy(cp.CertSANs, o.CertSANs)