diff --git a/cmd/kubeadm/app/apis/output/register.go b/cmd/kubeadm/app/apis/output/register.go index 4eb647d8e3a..0b9f8f87b1f 100644 --- a/cmd/kubeadm/app/apis/output/register.go +++ b/cmd/kubeadm/app/apis/output/register.go @@ -39,6 +39,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { &BootstrapToken{}, &Images{}, &UpgradePlan{}, + &CertificateExpirationInfo{}, ) return nil } diff --git a/cmd/kubeadm/app/apis/output/scheme/scheme.go b/cmd/kubeadm/app/apis/output/scheme/scheme.go index 27b033959b5..67c9ab9a604 100644 --- a/cmd/kubeadm/app/apis/output/scheme/scheme.go +++ b/cmd/kubeadm/app/apis/output/scheme/scheme.go @@ -25,6 +25,7 @@ import ( "k8s.io/kubernetes/cmd/kubeadm/app/apis/output" "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/v1alpha2" + "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/v1alpha3" ) // Scheme is the runtime.Scheme to which all kubeadm api types are registered. @@ -42,5 +43,6 @@ func init() { func AddToScheme(scheme *runtime.Scheme) { utilruntime.Must(output.AddToScheme(scheme)) utilruntime.Must(v1alpha2.AddToScheme(scheme)) - utilruntime.Must(scheme.SetVersionPriority(v1alpha2.SchemeGroupVersion)) + utilruntime.Must(v1alpha3.AddToScheme(scheme)) + utilruntime.Must(scheme.SetVersionPriority(v1alpha3.SchemeGroupVersion, v1alpha2.SchemeGroupVersion)) } diff --git a/cmd/kubeadm/app/apis/output/types.go b/cmd/kubeadm/app/apis/output/types.go index 9dda9491425..4f4fa33d845 100644 --- a/cmd/kubeadm/app/apis/output/types.go +++ b/cmd/kubeadm/app/apis/output/types.go @@ -82,3 +82,38 @@ type UpgradePlan struct { ConfigVersions []ComponentConfigVersionState } + +// Certificate represents information for a certificate or a certificate authority when using the check-expiration command. +type Certificate struct { + // Name of the certificate. + Name string + + // ExpirationDate defines certificate expiration date in UTC following the RFC3339 format. + ExpirationDate metav1.Time + + // ResidualTimeSeconds represents the duration in seconds relative to the residual time before expiration. + ResidualTimeSeconds int64 + + // ExternallyManaged defines if the certificate is externally managed. + ExternallyManaged bool + + // CAName represents the name of the CA that signed the certificate. + // This field is empty for self-signed, root CA certificates. + CAName string + + // Missing represents if the certificate is missing. + Missing bool +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CertificateExpirationInfo represents information for the output produced by 'kubeadm certs check-expiration'. +type CertificateExpirationInfo struct { + metav1.TypeMeta + + // Certificates holds a list of certificates to show expiration information for. + Certificates []Certificate + + // CertificateAuthorities holds a list of certificate authorities to show expiration information for. + CertificateAuthorities []Certificate +} diff --git a/cmd/kubeadm/app/apis/output/v1alpha2/doc.go b/cmd/kubeadm/app/apis/output/v1alpha2/doc.go index 4dacf725f7e..20e7e246db1 100644 --- a/cmd/kubeadm/app/apis/output/v1alpha2/doc.go +++ b/cmd/kubeadm/app/apis/output/v1alpha2/doc.go @@ -23,4 +23,5 @@ limitations under the License. // The purpose of the kubeadm structured output is to have a well // defined versioned output format that other software that uses // kubeadm for cluster deployments can use and rely on. +// DEPRECATED: this API will be removed in a future release. Please use v1alpha3. package v1alpha2 // import "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/v1alpha2" diff --git a/cmd/kubeadm/app/apis/output/v1alpha3/doc.go b/cmd/kubeadm/app/apis/output/v1alpha3/doc.go new file mode 100644 index 00000000000..73e66ba4f6f --- /dev/null +++ b/cmd/kubeadm/app/apis/output/v1alpha3/doc.go @@ -0,0 +1,30 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +groupName=output.kubeadm.k8s.io +// +k8s:deepcopy-gen=package +// +k8s:conversion-gen=k8s.io/kubernetes/cmd/kubeadm/app/apis/output + +// Package v1alpha3 defines the v1alpha3 version of the kubeadm data structures +// related to structured output +// The purpose of the kubeadm structured output is to have a well +// defined versioned output format that other software that uses +// kubeadm for cluster deployments can use and rely on. +// +// Changes since v1alpha2: +// - Added support for outputting certificate expiration information for "kubeadm certs check-expiration" +// with the CertificateExpirationInfo structure. +package v1alpha3 // import "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/v1alpha3" diff --git a/cmd/kubeadm/app/apis/output/v1alpha3/register.go b/cmd/kubeadm/app/apis/output/v1alpha3/register.go new file mode 100644 index 00000000000..099147b64cb --- /dev/null +++ b/cmd/kubeadm/app/apis/output/v1alpha3/register.go @@ -0,0 +1,55 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha3 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName is the group name use in this package +const GroupName = "output.kubeadm.k8s.io" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha3"} + +var ( + // SchemeBuilder points to a list of functions added to Scheme. + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + // AddToScheme applies all the stored functions to the scheme. + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + // We only register manually written functions here. The registration of the + // generated functions takes place in the generated files. The separation + // makes the code compile even when the generated files are missing. + localSchemeBuilder.Register(addKnownTypes) +} + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &BootstrapToken{}, + &Images{}, + &UpgradePlan{}, + &CertificateExpirationInfo{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/cmd/kubeadm/app/apis/output/v1alpha3/types.go b/cmd/kubeadm/app/apis/output/v1alpha3/types.go new file mode 100644 index 00000000000..0311de1e95c --- /dev/null +++ b/cmd/kubeadm/app/apis/output/v1alpha3/types.go @@ -0,0 +1,119 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha3 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + bootstraptokenv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/bootstraptoken/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// BootstrapToken represents information for the bootstrap token output produced by kubeadm +type BootstrapToken struct { + metav1.TypeMeta `json:",inline"` + + bootstraptokenv1.BootstrapToken +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Images represents information for the output produced by 'kubeadm config images list' +type Images struct { + metav1.TypeMeta `json:",inline"` + + Images []string `json:"images"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ComponentUpgradePlan represents information about upgrade plan for one component +type ComponentUpgradePlan struct { + metav1.TypeMeta + + Name string `json:"name"` + CurrentVersion string `json:"currentVersion"` + NewVersion string `json:"newVersion"` +} + +// ComponentConfigVersionState describes the current and desired version of a component config +type ComponentConfigVersionState struct { + // Group points to the Kubernetes API group that covers the config + Group string `json:"group"` + + // CurrentVersion is the currently active component config version + // NOTE: This can be empty in case the config was not found on the cluster or it was unsupported + // kubeadm generated version + CurrentVersion string `json:"currentVersion"` + + // PreferredVersion is the component config version that is currently preferred by kubeadm for use. + // NOTE: As of today, this is the only version supported by kubeadm. + PreferredVersion string `json:"preferredVersion"` + + // ManualUpgradeRequired indicates if users need to manually upgrade their component config versions. This happens if + // the CurrentVersion of the config is user supplied (or modified) and no longer supported. Users should upgrade + // their component configs to PreferredVersion or any other supported component config version. + ManualUpgradeRequired bool `json:"manualUpgradeRequired"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// UpgradePlan represents information about upgrade plan for the output +// produced by 'kubeadm upgrade plan' +type UpgradePlan struct { + metav1.TypeMeta + + Components []ComponentUpgradePlan `json:"components"` + + ConfigVersions []ComponentConfigVersionState `json:"configVersions"` +} + +// Certificate represents information for a certificate or a certificate authority when using the check-expiration command. +type Certificate struct { + // Name of the certificate. + Name string `json:"name"` + + // ExpirationDate defines certificate expiration date in UTC following the RFC3339 format. + ExpirationDate metav1.Time `json:"expirationDate"` + + // ResidualTimeSeconds represents the duration in seconds relative to the residual time before expiration. + ResidualTimeSeconds int64 `json:"residualTime"` + + // ExternallyManaged defines if the certificate is externally managed. + ExternallyManaged bool `json:"externallyManaged"` + + // CAName represents the name of the CA that signed the certificate. + // This field is empty for self-signed, root CA certificates. + CAName string `json:"caName,omitempty"` + + // Missing represents if the certificate is missing. + Missing bool `json:"missing"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CertificateExpirationInfo represents information for the output produced by 'kubeadm certs check-expiration'. +type CertificateExpirationInfo struct { + metav1.TypeMeta + + // Certificates holds a list of certificates to show expiration information for. + Certificates []Certificate `json:"certificates"` + + // CertificateAuthorities holds a list of certificate authorities to show expiration information for. + CertificateAuthorities []Certificate `json:"certificateAuthorities"` +} diff --git a/cmd/kubeadm/app/apis/output/v1alpha3/zz_generated.conversion.go b/cmd/kubeadm/app/apis/output/v1alpha3/zz_generated.conversion.go new file mode 100644 index 00000000000..6c4915c74ba --- /dev/null +++ b/cmd/kubeadm/app/apis/output/v1alpha3/zz_generated.conversion.go @@ -0,0 +1,274 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by conversion-gen. DO NOT EDIT. + +package v1alpha3 + +import ( + unsafe "unsafe" + + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" + output "k8s.io/kubernetes/cmd/kubeadm/app/apis/output" +) + +func init() { + localSchemeBuilder.Register(RegisterConversions) +} + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*BootstrapToken)(nil), (*output.BootstrapToken)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_BootstrapToken_To_output_BootstrapToken(a.(*BootstrapToken), b.(*output.BootstrapToken), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*output.BootstrapToken)(nil), (*BootstrapToken)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_output_BootstrapToken_To_v1alpha3_BootstrapToken(a.(*output.BootstrapToken), b.(*BootstrapToken), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*Certificate)(nil), (*output.Certificate)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_Certificate_To_output_Certificate(a.(*Certificate), b.(*output.Certificate), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*output.Certificate)(nil), (*Certificate)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_output_Certificate_To_v1alpha3_Certificate(a.(*output.Certificate), b.(*Certificate), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*CertificateExpirationInfo)(nil), (*output.CertificateExpirationInfo)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_CertificateExpirationInfo_To_output_CertificateExpirationInfo(a.(*CertificateExpirationInfo), b.(*output.CertificateExpirationInfo), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*output.CertificateExpirationInfo)(nil), (*CertificateExpirationInfo)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_output_CertificateExpirationInfo_To_v1alpha3_CertificateExpirationInfo(a.(*output.CertificateExpirationInfo), b.(*CertificateExpirationInfo), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ComponentConfigVersionState)(nil), (*output.ComponentConfigVersionState)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_ComponentConfigVersionState_To_output_ComponentConfigVersionState(a.(*ComponentConfigVersionState), b.(*output.ComponentConfigVersionState), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*output.ComponentConfigVersionState)(nil), (*ComponentConfigVersionState)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_output_ComponentConfigVersionState_To_v1alpha3_ComponentConfigVersionState(a.(*output.ComponentConfigVersionState), b.(*ComponentConfigVersionState), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*ComponentUpgradePlan)(nil), (*output.ComponentUpgradePlan)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_ComponentUpgradePlan_To_output_ComponentUpgradePlan(a.(*ComponentUpgradePlan), b.(*output.ComponentUpgradePlan), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*output.ComponentUpgradePlan)(nil), (*ComponentUpgradePlan)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_output_ComponentUpgradePlan_To_v1alpha3_ComponentUpgradePlan(a.(*output.ComponentUpgradePlan), b.(*ComponentUpgradePlan), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*Images)(nil), (*output.Images)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_Images_To_output_Images(a.(*Images), b.(*output.Images), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*output.Images)(nil), (*Images)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_output_Images_To_v1alpha3_Images(a.(*output.Images), b.(*Images), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*UpgradePlan)(nil), (*output.UpgradePlan)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha3_UpgradePlan_To_output_UpgradePlan(a.(*UpgradePlan), b.(*output.UpgradePlan), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*output.UpgradePlan)(nil), (*UpgradePlan)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_output_UpgradePlan_To_v1alpha3_UpgradePlan(a.(*output.UpgradePlan), b.(*UpgradePlan), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvert_v1alpha3_BootstrapToken_To_output_BootstrapToken(in *BootstrapToken, out *output.BootstrapToken, s conversion.Scope) error { + out.BootstrapToken = in.BootstrapToken + return nil +} + +// Convert_v1alpha3_BootstrapToken_To_output_BootstrapToken is an autogenerated conversion function. +func Convert_v1alpha3_BootstrapToken_To_output_BootstrapToken(in *BootstrapToken, out *output.BootstrapToken, s conversion.Scope) error { + return autoConvert_v1alpha3_BootstrapToken_To_output_BootstrapToken(in, out, s) +} + +func autoConvert_output_BootstrapToken_To_v1alpha3_BootstrapToken(in *output.BootstrapToken, out *BootstrapToken, s conversion.Scope) error { + out.BootstrapToken = in.BootstrapToken + return nil +} + +// Convert_output_BootstrapToken_To_v1alpha3_BootstrapToken is an autogenerated conversion function. +func Convert_output_BootstrapToken_To_v1alpha3_BootstrapToken(in *output.BootstrapToken, out *BootstrapToken, s conversion.Scope) error { + return autoConvert_output_BootstrapToken_To_v1alpha3_BootstrapToken(in, out, s) +} + +func autoConvert_v1alpha3_Certificate_To_output_Certificate(in *Certificate, out *output.Certificate, s conversion.Scope) error { + out.Name = in.Name + out.ExpirationDate = in.ExpirationDate + out.ResidualTimeSeconds = in.ResidualTimeSeconds + out.ExternallyManaged = in.ExternallyManaged + out.CAName = in.CAName + out.Missing = in.Missing + return nil +} + +// Convert_v1alpha3_Certificate_To_output_Certificate is an autogenerated conversion function. +func Convert_v1alpha3_Certificate_To_output_Certificate(in *Certificate, out *output.Certificate, s conversion.Scope) error { + return autoConvert_v1alpha3_Certificate_To_output_Certificate(in, out, s) +} + +func autoConvert_output_Certificate_To_v1alpha3_Certificate(in *output.Certificate, out *Certificate, s conversion.Scope) error { + out.Name = in.Name + out.ExpirationDate = in.ExpirationDate + out.ResidualTimeSeconds = in.ResidualTimeSeconds + out.ExternallyManaged = in.ExternallyManaged + out.CAName = in.CAName + out.Missing = in.Missing + return nil +} + +// Convert_output_Certificate_To_v1alpha3_Certificate is an autogenerated conversion function. +func Convert_output_Certificate_To_v1alpha3_Certificate(in *output.Certificate, out *Certificate, s conversion.Scope) error { + return autoConvert_output_Certificate_To_v1alpha3_Certificate(in, out, s) +} + +func autoConvert_v1alpha3_CertificateExpirationInfo_To_output_CertificateExpirationInfo(in *CertificateExpirationInfo, out *output.CertificateExpirationInfo, s conversion.Scope) error { + out.Certificates = *(*[]output.Certificate)(unsafe.Pointer(&in.Certificates)) + out.CertificateAuthorities = *(*[]output.Certificate)(unsafe.Pointer(&in.CertificateAuthorities)) + return nil +} + +// Convert_v1alpha3_CertificateExpirationInfo_To_output_CertificateExpirationInfo is an autogenerated conversion function. +func Convert_v1alpha3_CertificateExpirationInfo_To_output_CertificateExpirationInfo(in *CertificateExpirationInfo, out *output.CertificateExpirationInfo, s conversion.Scope) error { + return autoConvert_v1alpha3_CertificateExpirationInfo_To_output_CertificateExpirationInfo(in, out, s) +} + +func autoConvert_output_CertificateExpirationInfo_To_v1alpha3_CertificateExpirationInfo(in *output.CertificateExpirationInfo, out *CertificateExpirationInfo, s conversion.Scope) error { + out.Certificates = *(*[]Certificate)(unsafe.Pointer(&in.Certificates)) + out.CertificateAuthorities = *(*[]Certificate)(unsafe.Pointer(&in.CertificateAuthorities)) + return nil +} + +// Convert_output_CertificateExpirationInfo_To_v1alpha3_CertificateExpirationInfo is an autogenerated conversion function. +func Convert_output_CertificateExpirationInfo_To_v1alpha3_CertificateExpirationInfo(in *output.CertificateExpirationInfo, out *CertificateExpirationInfo, s conversion.Scope) error { + return autoConvert_output_CertificateExpirationInfo_To_v1alpha3_CertificateExpirationInfo(in, out, s) +} + +func autoConvert_v1alpha3_ComponentConfigVersionState_To_output_ComponentConfigVersionState(in *ComponentConfigVersionState, out *output.ComponentConfigVersionState, s conversion.Scope) error { + out.Group = in.Group + out.CurrentVersion = in.CurrentVersion + out.PreferredVersion = in.PreferredVersion + out.ManualUpgradeRequired = in.ManualUpgradeRequired + return nil +} + +// Convert_v1alpha3_ComponentConfigVersionState_To_output_ComponentConfigVersionState is an autogenerated conversion function. +func Convert_v1alpha3_ComponentConfigVersionState_To_output_ComponentConfigVersionState(in *ComponentConfigVersionState, out *output.ComponentConfigVersionState, s conversion.Scope) error { + return autoConvert_v1alpha3_ComponentConfigVersionState_To_output_ComponentConfigVersionState(in, out, s) +} + +func autoConvert_output_ComponentConfigVersionState_To_v1alpha3_ComponentConfigVersionState(in *output.ComponentConfigVersionState, out *ComponentConfigVersionState, s conversion.Scope) error { + out.Group = in.Group + out.CurrentVersion = in.CurrentVersion + out.PreferredVersion = in.PreferredVersion + out.ManualUpgradeRequired = in.ManualUpgradeRequired + return nil +} + +// Convert_output_ComponentConfigVersionState_To_v1alpha3_ComponentConfigVersionState is an autogenerated conversion function. +func Convert_output_ComponentConfigVersionState_To_v1alpha3_ComponentConfigVersionState(in *output.ComponentConfigVersionState, out *ComponentConfigVersionState, s conversion.Scope) error { + return autoConvert_output_ComponentConfigVersionState_To_v1alpha3_ComponentConfigVersionState(in, out, s) +} + +func autoConvert_v1alpha3_ComponentUpgradePlan_To_output_ComponentUpgradePlan(in *ComponentUpgradePlan, out *output.ComponentUpgradePlan, s conversion.Scope) error { + out.Name = in.Name + out.CurrentVersion = in.CurrentVersion + out.NewVersion = in.NewVersion + return nil +} + +// Convert_v1alpha3_ComponentUpgradePlan_To_output_ComponentUpgradePlan is an autogenerated conversion function. +func Convert_v1alpha3_ComponentUpgradePlan_To_output_ComponentUpgradePlan(in *ComponentUpgradePlan, out *output.ComponentUpgradePlan, s conversion.Scope) error { + return autoConvert_v1alpha3_ComponentUpgradePlan_To_output_ComponentUpgradePlan(in, out, s) +} + +func autoConvert_output_ComponentUpgradePlan_To_v1alpha3_ComponentUpgradePlan(in *output.ComponentUpgradePlan, out *ComponentUpgradePlan, s conversion.Scope) error { + out.Name = in.Name + out.CurrentVersion = in.CurrentVersion + out.NewVersion = in.NewVersion + return nil +} + +// Convert_output_ComponentUpgradePlan_To_v1alpha3_ComponentUpgradePlan is an autogenerated conversion function. +func Convert_output_ComponentUpgradePlan_To_v1alpha3_ComponentUpgradePlan(in *output.ComponentUpgradePlan, out *ComponentUpgradePlan, s conversion.Scope) error { + return autoConvert_output_ComponentUpgradePlan_To_v1alpha3_ComponentUpgradePlan(in, out, s) +} + +func autoConvert_v1alpha3_Images_To_output_Images(in *Images, out *output.Images, s conversion.Scope) error { + out.Images = *(*[]string)(unsafe.Pointer(&in.Images)) + return nil +} + +// Convert_v1alpha3_Images_To_output_Images is an autogenerated conversion function. +func Convert_v1alpha3_Images_To_output_Images(in *Images, out *output.Images, s conversion.Scope) error { + return autoConvert_v1alpha3_Images_To_output_Images(in, out, s) +} + +func autoConvert_output_Images_To_v1alpha3_Images(in *output.Images, out *Images, s conversion.Scope) error { + out.Images = *(*[]string)(unsafe.Pointer(&in.Images)) + return nil +} + +// Convert_output_Images_To_v1alpha3_Images is an autogenerated conversion function. +func Convert_output_Images_To_v1alpha3_Images(in *output.Images, out *Images, s conversion.Scope) error { + return autoConvert_output_Images_To_v1alpha3_Images(in, out, s) +} + +func autoConvert_v1alpha3_UpgradePlan_To_output_UpgradePlan(in *UpgradePlan, out *output.UpgradePlan, s conversion.Scope) error { + out.Components = *(*[]output.ComponentUpgradePlan)(unsafe.Pointer(&in.Components)) + out.ConfigVersions = *(*[]output.ComponentConfigVersionState)(unsafe.Pointer(&in.ConfigVersions)) + return nil +} + +// Convert_v1alpha3_UpgradePlan_To_output_UpgradePlan is an autogenerated conversion function. +func Convert_v1alpha3_UpgradePlan_To_output_UpgradePlan(in *UpgradePlan, out *output.UpgradePlan, s conversion.Scope) error { + return autoConvert_v1alpha3_UpgradePlan_To_output_UpgradePlan(in, out, s) +} + +func autoConvert_output_UpgradePlan_To_v1alpha3_UpgradePlan(in *output.UpgradePlan, out *UpgradePlan, s conversion.Scope) error { + out.Components = *(*[]ComponentUpgradePlan)(unsafe.Pointer(&in.Components)) + out.ConfigVersions = *(*[]ComponentConfigVersionState)(unsafe.Pointer(&in.ConfigVersions)) + return nil +} + +// Convert_output_UpgradePlan_To_v1alpha3_UpgradePlan is an autogenerated conversion function. +func Convert_output_UpgradePlan_To_v1alpha3_UpgradePlan(in *output.UpgradePlan, out *UpgradePlan, s conversion.Scope) error { + return autoConvert_output_UpgradePlan_To_v1alpha3_UpgradePlan(in, out, s) +} diff --git a/cmd/kubeadm/app/apis/output/v1alpha3/zz_generated.deepcopy.go b/cmd/kubeadm/app/apis/output/v1alpha3/zz_generated.deepcopy.go new file mode 100644 index 00000000000..f8a93667bb6 --- /dev/null +++ b/cmd/kubeadm/app/apis/output/v1alpha3/zz_generated.deepcopy.go @@ -0,0 +1,214 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha3 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BootstrapToken) DeepCopyInto(out *BootstrapToken) { + *out = *in + out.TypeMeta = in.TypeMeta + in.BootstrapToken.DeepCopyInto(&out.BootstrapToken) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BootstrapToken. +func (in *BootstrapToken) DeepCopy() *BootstrapToken { + if in == nil { + return nil + } + out := new(BootstrapToken) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BootstrapToken) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Certificate) DeepCopyInto(out *Certificate) { + *out = *in + in.ExpirationDate.DeepCopyInto(&out.ExpirationDate) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Certificate. +func (in *Certificate) DeepCopy() *Certificate { + if in == nil { + return nil + } + out := new(Certificate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateExpirationInfo) DeepCopyInto(out *CertificateExpirationInfo) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Certificates != nil { + in, out := &in.Certificates, &out.Certificates + *out = make([]Certificate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.CertificateAuthorities != nil { + in, out := &in.CertificateAuthorities, &out.CertificateAuthorities + *out = make([]Certificate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateExpirationInfo. +func (in *CertificateExpirationInfo) DeepCopy() *CertificateExpirationInfo { + if in == nil { + return nil + } + out := new(CertificateExpirationInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CertificateExpirationInfo) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComponentConfigVersionState) DeepCopyInto(out *ComponentConfigVersionState) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentConfigVersionState. +func (in *ComponentConfigVersionState) DeepCopy() *ComponentConfigVersionState { + if in == nil { + return nil + } + out := new(ComponentConfigVersionState) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComponentUpgradePlan) DeepCopyInto(out *ComponentUpgradePlan) { + *out = *in + out.TypeMeta = in.TypeMeta + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentUpgradePlan. +func (in *ComponentUpgradePlan) DeepCopy() *ComponentUpgradePlan { + if in == nil { + return nil + } + out := new(ComponentUpgradePlan) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ComponentUpgradePlan) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Images) DeepCopyInto(out *Images) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Images != nil { + in, out := &in.Images, &out.Images + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Images. +func (in *Images) DeepCopy() *Images { + if in == nil { + return nil + } + out := new(Images) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Images) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UpgradePlan) DeepCopyInto(out *UpgradePlan) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Components != nil { + in, out := &in.Components, &out.Components + *out = make([]ComponentUpgradePlan, len(*in)) + copy(*out, *in) + } + if in.ConfigVersions != nil { + in, out := &in.ConfigVersions, &out.ConfigVersions + *out = make([]ComponentConfigVersionState, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpgradePlan. +func (in *UpgradePlan) DeepCopy() *UpgradePlan { + if in == nil { + return nil + } + out := new(UpgradePlan) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *UpgradePlan) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go b/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go index 8d553d35c15..098e52697ef 100644 --- a/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go +++ b/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go @@ -51,6 +51,62 @@ func (in *BootstrapToken) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Certificate) DeepCopyInto(out *Certificate) { + *out = *in + in.ExpirationDate.DeepCopyInto(&out.ExpirationDate) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Certificate. +func (in *Certificate) DeepCopy() *Certificate { + if in == nil { + return nil + } + out := new(Certificate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificateExpirationInfo) DeepCopyInto(out *CertificateExpirationInfo) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Certificates != nil { + in, out := &in.Certificates, &out.Certificates + *out = make([]Certificate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.CertificateAuthorities != nil { + in, out := &in.CertificateAuthorities, &out.CertificateAuthorities + *out = make([]Certificate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificateExpirationInfo. +func (in *CertificateExpirationInfo) DeepCopy() *CertificateExpirationInfo { + if in == nil { + return nil + } + out := new(CertificateExpirationInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CertificateExpirationInfo) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ComponentConfigVersionState) DeepCopyInto(out *ComponentConfigVersionState) { *out = *in diff --git a/cmd/kubeadm/app/cmd/certs.go b/cmd/kubeadm/app/cmd/certs.go index c0518c36971..3c12fd58e28 100644 --- a/cmd/kubeadm/app/cmd/certs.go +++ b/cmd/kubeadm/app/cmd/certs.go @@ -20,17 +20,25 @@ import ( "fmt" "io" "text/tabwriter" + "time" "github.com/lithammer/dedent" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" + outputapischeme "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/scheme" + outputapiv1alpha3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/v1alpha3" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" @@ -40,6 +48,7 @@ import ( kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" + "k8s.io/kubernetes/cmd/kubeadm/app/util/output" ) var ( @@ -252,7 +261,8 @@ func getRenewSubCommands(out io.Writer, kdir string) []*cobra.Command { renewalFunc := func(handler *renewal.CertificateRenewHandler) func() error { return func() error { // Get cluster configuration (from --config, kubeadm-config ConfigMap, or default as a fallback) - internalcfg, err := getInternalCfg(flags.cfgPath, flags.kubeconfigPath, flags.cfg, out, "renew") + client, _ := kubeconfigutil.ClientSetFromFile(flags.kubeconfigPath) + internalcfg, err := getInternalCfg(flags.cfgPath, client, flags.cfg, &output.TextPrinter{}, "renew") if err != nil { return err } @@ -272,7 +282,8 @@ func getRenewSubCommands(out io.Writer, kdir string) []*cobra.Command { Long: allLongDesc, RunE: func(*cobra.Command, []string) error { // Get cluster configuration (from --config, kubeadm-config ConfigMap, or default as a fallback) - internalcfg, err := getInternalCfg(flags.cfgPath, flags.kubeconfigPath, flags.cfg, out, "renew") + client, _ := kubeconfigutil.ClientSetFromFile(flags.kubeconfigPath) + internalcfg, err := getInternalCfg(flags.cfgPath, client, flags.cfg, &output.TextPrinter{}, "renew") if err != nil { return err } @@ -332,27 +343,24 @@ func renewCert(kdir string, internalcfg *kubeadmapi.InitConfiguration, handler * return nil } -func getInternalCfg(cfgPath string, kubeconfigPath string, cfg kubeadmapiv1.ClusterConfiguration, out io.Writer, logPrefix string) (*kubeadmapi.InitConfiguration, error) { +func getInternalCfg(cfgPath string, client kubernetes.Interface, cfg kubeadmapiv1.ClusterConfiguration, printer output.Printer, logPrefix string) (*kubeadmapi.InitConfiguration, error) { // In case the user is not providing a custom config, try to get current config from the cluster. // NB. this operation should not block, because we want to allow certificate renewal also in case of not-working clusters - if cfgPath == "" { - client, err := kubeconfigutil.ClientSetFromFile(kubeconfigPath) + if cfgPath == "" && client != nil { + internalcfg, err := configutil.FetchInitConfigurationFromCluster(client, printer, logPrefix, false, false) if err == nil { - internalcfg, err := configutil.FetchInitConfigurationFromCluster(client, nil, logPrefix, false, false) - if err == nil { - fmt.Println() // add empty line to separate the FetchInitConfigurationFromCluster output from the command output - // certificate renewal or expiration checking doesn't depend on a running cluster, which means the CertificatesDir - // could be set to a value other than the default value or the value fetched from the cluster. - // cfg.CertificatesDir could be empty if the default value is set to empty (not true today). - if len(cfg.CertificatesDir) != 0 { - klog.V(1).Infof("Overriding the cluster certificate directory with the value from command line flag --%s: %s", options.CertificatesDir, cfg.CertificatesDir) - internalcfg.ClusterConfiguration.CertificatesDir = cfg.CertificatesDir - } - - return internalcfg, nil + printer.Println() // add empty line to separate the FetchInitConfigurationFromCluster output from the command output + // certificate renewal or expiration checking doesn't depend on a running cluster, which means the CertificatesDir + // could be set to a value other than the default value or the value fetched from the cluster. + // cfg.CertificatesDir could be empty if the default value is set to empty (not true today). + if len(cfg.CertificatesDir) != 0 { + klog.V(1).Infof("Overriding the cluster certificate directory with the value from command line flag --%s: %s", options.CertificatesDir, cfg.CertificatesDir) + internalcfg.ClusterConfiguration.CertificatesDir = cfg.CertificatesDir } - fmt.Printf("[%s] Error reading configuration from the Cluster. Falling back to default configuration\n\n", logPrefix) + + return internalcfg, nil } + printer.Printf("[%s] Error reading configuration from the Cluster. Falling back to default configuration\n\n", logPrefix) } // Read config from --config if provided. Otherwise, use the default configuration @@ -361,6 +369,60 @@ func getInternalCfg(cfgPath string, kubeconfigPath string, cfg kubeadmapiv1.Clus }) } +// fetchCertificateExpirationInfo returns the certificate expiration info for the given renewal manager +func fetchCertificateExpirationInfo(rm *renewal.Manager) (*outputapiv1alpha3.CertificateExpirationInfo, error) { + info := &outputapiv1alpha3.CertificateExpirationInfo{} + + for _, handler := range rm.Certificates() { + if ok, _ := rm.CertificateExists(handler.Name); ok { + e, err := rm.GetCertificateExpirationInfo(handler.Name) + if err != nil { + return nil, err + } + info.Certificates = append(info.Certificates, outputapiv1alpha3.Certificate{ + Name: e.Name, + ExpirationDate: metav1.Time{Time: e.ExpirationDate}, + ResidualTimeSeconds: int64(e.ResidualTime() / time.Second), + CAName: handler.CAName, + ExternallyManaged: e.ExternallyManaged, + }) + } else { + // the certificate does not exist (for any reason) + info.Certificates = append(info.Certificates, outputapiv1alpha3.Certificate{ + Name: handler.Name, + Missing: true, + }) + } + } + + for _, handler := range rm.CAs() { + if ok, _ := rm.CAExists(handler.Name); ok { + e, err := rm.GetCAExpirationInfo(handler.Name) + if err != nil { + return nil, err + } + info.CertificateAuthorities = append(info.CertificateAuthorities, outputapiv1alpha3.Certificate{ + Name: e.Name, + ExpirationDate: metav1.Time{Time: e.ExpirationDate}, + ResidualTimeSeconds: int64(e.ResidualTime() / time.Second), + ExternallyManaged: e.ExternallyManaged, + }) + } else { + // the CA does not exist (for any reason) + info.CertificateAuthorities = append(info.CertificateAuthorities, outputapiv1alpha3.Certificate{ + Name: handler.Name, + Missing: true, + }) + } + } + + return info, nil +} + +// clientSetFromFile is a variable that holds the function to create a clientset from a kubeconfig file. +// It is used for testing purposes. +var clientSetFromFile = kubeconfigutil.ClientSetFromFile + // newCmdCertsExpiration creates a new `cert check-expiration` command. func newCmdCertsExpiration(out io.Writer, kdir string) *cobra.Command { flags := &expirationFlags{ @@ -373,13 +435,21 @@ func newCmdCertsExpiration(out io.Writer, kdir string) *cobra.Command { // Default values for the cobra help text kubeadmscheme.Scheme.Default(&flags.cfg) + outputFlags := output.NewOutputFlags(&certTextPrintFlags{}).WithTypeSetter(outputapischeme.Scheme).WithDefaultOutput(output.TextOutput) + cmd := &cobra.Command{ Use: "check-expiration", Short: "Check certificates expiration for a Kubernetes cluster", Long: expirationLongDesc, RunE: func(cmd *cobra.Command, args []string) error { + printer, err := outputFlags.ToPrinter() + if err != nil { + return errors.Wrap(err, "could not construct output printer") + } + // Get cluster configuration (from --config, kubeadm-config ConfigMap, or default as a fallback) - internalcfg, err := getInternalCfg(flags.cfgPath, flags.kubeconfigPath, flags.cfg, out, "check-expiration") + client, _ := clientSetFromFile(flags.kubeconfigPath) + internalcfg, err := getInternalCfg(flags.cfgPath, client, flags.cfg, printer, "check-expiration") if err != nil { return err } @@ -390,73 +460,16 @@ func newCmdCertsExpiration(out io.Writer, kdir string) *cobra.Command { return err } - // Get all the certificate expiration info - yesNo := func(b bool) string { - if b { - return "yes" - } - return "no" + info, err := fetchCertificateExpirationInfo(rm) + if err != nil { + return err } - w := tabwriter.NewWriter(out, 10, 4, 3, ' ', 0) - fmt.Fprintln(w, "CERTIFICATE\tEXPIRES\tRESIDUAL TIME\tCERTIFICATE AUTHORITY\tEXTERNALLY MANAGED") - for _, handler := range rm.Certificates() { - if ok, _ := rm.CertificateExists(handler.Name); ok { - e, err := rm.GetCertificateExpirationInfo(handler.Name) - if err != nil { - return err - } - - s := fmt.Sprintf("%s\t%s\t%s\t%s\t%-8v", - e.Name, - e.ExpirationDate.Format("Jan 02, 2006 15:04 MST"), - duration.ShortHumanDuration(e.ResidualTime()), - handler.CAName, - yesNo(e.ExternallyManaged), - ) - - fmt.Fprintln(w, s) - continue - } - - // the certificate does not exist (for any reason) - s := fmt.Sprintf("!MISSING! %s\t\t\t\t", - handler.Name, - ) - fmt.Fprintln(w, s) - } - fmt.Fprintln(w) - fmt.Fprintln(w, "CERTIFICATE AUTHORITY\tEXPIRES\tRESIDUAL TIME\tEXTERNALLY MANAGED") - for _, handler := range rm.CAs() { - if ok, _ := rm.CAExists(handler.Name); ok { - e, err := rm.GetCAExpirationInfo(handler.Name) - if err != nil { - return err - } - - s := fmt.Sprintf("%s\t%s\t%s\t%-8v", - e.Name, - e.ExpirationDate.Format("Jan 02, 2006 15:04 MST"), - duration.ShortHumanDuration(e.ResidualTime()), - yesNo(e.ExternallyManaged), - ) - - fmt.Fprintln(w, s) - continue - } - - // the CA does not exist (for any reason) - s := fmt.Sprintf("!MISSING! %s\t\t\t", - handler.Name, - ) - fmt.Fprintln(w, s) - } - w.Flush() - return nil + return printer.PrintObj(info, out) }, Args: cobra.NoArgs, } addExpirationFlags(cmd, flags) - + outputFlags.AddFlags(cmd) return cmd } @@ -471,3 +484,72 @@ func addExpirationFlags(cmd *cobra.Command, flags *expirationFlags) { options.AddCertificateDirFlag(cmd.Flags(), &flags.cfg.CertificatesDir) options.AddKubeConfigFlag(cmd.Flags(), &flags.kubeconfigPath) } + +// certsTextPrinter prints all certificates in a text form +type certTextPrinter struct { + output.TextPrinter +} + +// PrintObj is an implementation of ResourcePrinter.PrintObj for plain text output +func (p *certTextPrinter) PrintObj(obj runtime.Object, writer io.Writer) error { + info, ok := obj.(*outputapiv1alpha3.CertificateExpirationInfo) + if !ok { + return errors.New("unexpected type") + } + + yesNo := func(b bool) string { + if b { + return "yes" + } + return "no" + } + + tabw := tabwriter.NewWriter(writer, 10, 4, 3, ' ', 0) + fmt.Fprintln(tabw, "CERTIFICATE\tEXPIRES\tRESIDUAL TIME\tCERTIFICATE AUTHORITY\tEXTERNALLY MANAGED") + for _, cert := range info.Certificates { + if cert.Missing { + s := fmt.Sprintf("!MISSING! %s\t\t\t\t", cert.Name) + fmt.Fprintln(tabw, s) + continue + } + + s := fmt.Sprintf("%s\t%s\t%s\t%s\t%-8v", + cert.Name, + cert.ExpirationDate.Format("Jan 02, 2006 15:04 MST"), + duration.ShortHumanDuration(time.Duration(cert.ResidualTimeSeconds)*time.Second), + cert.CAName, + yesNo(cert.ExternallyManaged), + ) + fmt.Fprintln(tabw, s) + } + + fmt.Fprintln(tabw) + fmt.Fprintln(tabw, "CERTIFICATE AUTHORITY\tEXPIRES\tRESIDUAL TIME\tEXTERNALLY MANAGED") + for _, ca := range info.CertificateAuthorities { + if ca.Missing { + s := fmt.Sprintf("!MISSING! %s\t\t\t", ca.Name) + fmt.Fprintln(tabw, s) + continue + } + + s := fmt.Sprintf("%s\t%s\t%s\t%-8v", + ca.Name, + ca.ExpirationDate.Format("Jan 02, 2006 15:04 MST"), + duration.ShortHumanDuration(time.Duration(ca.ResidualTimeSeconds)*time.Second), + yesNo(ca.ExternallyManaged), + ) + fmt.Fprintln(tabw, s) + } + return tabw.Flush() +} + +// certTextPrintFlags provides flags necessary for printing +type certTextPrintFlags struct{} + +// ToPrinter returns a kubeadm printer for the text output format +func (tpf *certTextPrintFlags) ToPrinter(outputFormat string) (output.Printer, error) { + if outputFormat == output.TextOutput { + return &certTextPrinter{}, nil + } + return nil, genericclioptions.NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: []string{output.TextOutput}} +} diff --git a/cmd/kubeadm/app/cmd/certs_test.go b/cmd/kubeadm/app/cmd/certs_test.go index 3b50a6babe4..b6286a2ed2f 100644 --- a/cmd/kubeadm/app/cmd/certs_test.go +++ b/cmd/kubeadm/app/cmd/certs_test.go @@ -20,8 +20,10 @@ limitations under the License. package cmd import ( + "bytes" "crypto" "crypto/x509" + "encoding/json" "fmt" "io" "os" @@ -30,18 +32,27 @@ import ( "testing" "time" + "github.com/lithammer/dedent" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + fakeclientset "k8s.io/client-go/kubernetes/fake" + clientgotesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/clientcmd" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" + outputapiv1alpha3 "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/v1alpha3" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig" + kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" "k8s.io/kubernetes/cmd/kubeadm/app/util/pkiutil" testutil "k8s.io/kubernetes/cmd/kubeadm/test" cmdtestutil "k8s.io/kubernetes/cmd/kubeadm/test/cmd" @@ -482,3 +493,223 @@ kubernetesVersion: %s`, }) } } + +func TestRunCmdCertsExpiration(t *testing.T) { + kdir := testutil.SetupTempDir(t) + defer func() { + if err := os.RemoveAll(kdir); err != nil { + t.Fatalf("Failed to teardown: %s", err) + } + clientSetFromFile = kubeconfigutil.ClientSetFromFile + }() + + cfg := testutil.GetDefaultInternalConfig(t) + cfg.CertificatesDir = kdir + + // Generate all the CA + caCerts := map[string]*x509.Certificate{} + caKeys := map[string]crypto.Signer{} + for _, ca := range []*certsphase.KubeadmCert{ + certsphase.KubeadmCertRootCA(), + certsphase.KubeadmCertFrontProxyCA(), + certsphase.KubeadmCertEtcdCA(), + } { + caCert, caKey, err := ca.CreateAsCA(cfg) + if err != nil { + t.Fatalf("couldn't write out CA %s: %v", ca.Name, err) + } + caCerts[ca.Name] = caCert + caKeys[ca.Name] = caKey + } + + // Generate all the signed certificates + kubeadmCerts := []*certsphase.KubeadmCert{ + certsphase.KubeadmCertAPIServer(), + certsphase.KubeadmCertKubeletClient(), + certsphase.KubeadmCertFrontProxyClient(), + certsphase.KubeadmCertEtcdAPIClient(), + certsphase.KubeadmCertEtcdServer(), + certsphase.KubeadmCertEtcdPeer(), + certsphase.KubeadmCertEtcdHealthcheck(), + } + for _, cert := range kubeadmCerts { + caCert := caCerts[cert.CAName] + caKey := caKeys[cert.CAName] + if err := cert.CreateFromCA(cfg, caCert, caKey); err != nil { + t.Fatalf("couldn't write certificate %s: %v", cert.Name, err) + } + } + + // Generate all the kubeconfig files with embedded certs + kubeConfigs := []string{ + kubeadmconstants.AdminKubeConfigFileName, + kubeadmconstants.SuperAdminKubeConfigFileName, + kubeadmconstants.SchedulerKubeConfigFileName, + kubeadmconstants.ControllerManagerKubeConfigFileName, + } + for _, kubeConfig := range kubeConfigs { + if err := kubeconfigphase.CreateKubeConfigFile(kubeConfig, kdir, cfg); err != nil { + t.Fatalf("couldn't create kubeconfig %q: %v", kubeConfig, err) + } + } + + // A minimal kubeadm config with just enough values to avoid triggering + // auto-detection of config values at runtime. + var kubeadmConfig = fmt.Sprintf(` +apiVersion: %[1]s +kind: ClusterConfiguration +certificatesDir: %s +kubernetesVersion: %s`, + kubeadmapiv1.SchemeGroupVersion.String(), + cfg.CertificatesDir, + kubeadmconstants.MinimumControlPlaneVersion.String()) + + // Write the minimal kubeadm config to a file + customConfigPath := kdir + "/kubeadm.conf" + f, err := os.Create(customConfigPath) + require.NoError(t, err) + _, err = f.Write([]byte(kubeadmConfig)) + require.NoError(t, err) + + // fakeClientSetFromFile returns a fake clientset with kubeadm config map + var fakeClientSetFromFile = func(_ string) (kubernetes.Interface, error) { + client := fakeclientset.NewSimpleClientset() + client.PrependReactor("get", "configmaps", func(action clientgotesting.Action) (bool, runtime.Object, error) { + getAction := action.(clientgotesting.GetAction) + if getAction.GetNamespace() == metav1.NamespaceSystem && getAction.GetName() == kubeadmconstants.KubeadmConfigConfigMap { + cm := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: kubeadmconstants.KubeadmConfigConfigMap, + Namespace: metav1.NamespaceSystem, + }, + Data: map[string]string{ + kubeadmconstants.ClusterConfigurationConfigMapKey: dedent.Dedent(kubeadmConfig), + }, + } + return true, cm, nil + } + // Return an empty config map for kubelet and kube-proxy components. + return true, &v1.ConfigMap{}, nil + }) + return client, nil + } + + // Select a certificate used to simulate a missing certificate file + brokenCertName := kubeadmconstants.APIServerCertAndKeyBaseName + brokenCertPath, _ := pkiutil.PathsForCertAndKey(cfg.CertificatesDir, brokenCertName) + + type testCase struct { + name string + config string + output string + brokenCertName string + } + + var runTestCase = func(t *testing.T, tc testCase) { + var output bytes.Buffer + cmd := newCmdCertsExpiration(&output, kdir) + args := []string{ + fmt.Sprintf("--cert-dir=%s", cfg.CertificatesDir), + } + if tc.config != "" { + args = append(args, fmt.Sprintf("--config=%s", tc.config)) + } else { + clientSetFromFile = fakeClientSetFromFile + defer func() { + clientSetFromFile = kubeconfigutil.ClientSetFromFile + }() + } + if tc.output != "" { + args = append(args, fmt.Sprintf("-o=%s", tc.output)) + } + cmd.SetArgs(args) + require.NoError(t, cmd.Execute()) + + switch tc.output { + case "json": + var info outputapiv1alpha3.CertificateExpirationInfo + require.NoError(t, json.Unmarshal(output.Bytes(), &info)) + assert.Len(t, info.Certificates, len(kubeadmCerts)+len(kubeConfigs)) + assert.Len(t, info.CertificateAuthorities, len(caCerts)) + for _, cert := range info.Certificates { + if tc.brokenCertName == cert.Name { + assert.True(t, cert.Missing, "expected certificate to be missing") + } else { + assert.False(t, cert.Missing, "expected certificate to be present") + } + } + default: + outputStr := output.String() + if tc.brokenCertName != "" { + assert.Contains(t, outputStr, "!MISSING!") + } else { + assert.NotContains(t, outputStr, "!MISSING!") + } + + var lines []string + for _, line := range strings.SplitAfter(outputStr, "\n") { + if strings.TrimSpace(line) != "" { + lines = append(lines, line) + } + } + // 2 lines for the column headers. + expectedLineCount := len(caCerts) + len(kubeadmCerts) + len(kubeConfigs) + 2 + assert.Lenf(t, lines, expectedLineCount, "expected %d non-blank lines in output", expectedLineCount) + } + } + + testCases := []testCase{ + { + name: "fetch kubeadm config from file and print columns and no missing certs", + config: customConfigPath, + output: "", + }, + { + name: "fetch kubeadm config from server and print columns and no missing certs", + output: "", + }, + { + name: "fetch kubeadm config from file and print json and no missing certs", + config: customConfigPath, + output: "json", + }, + { + name: "fetch kubeadm config from server and print json and no missing certs", + output: "json", + }, + // all broken cases must be at the end of the list. + { + name: "fetch kubeadm config from file and print columns and missing certs", + config: customConfigPath, + output: "", + brokenCertName: brokenCertName, + }, + { + name: "fetch kubeadm config from server and print columns and missing certs", + output: "", + brokenCertName: brokenCertName, + }, + { + name: "fetch kubeadm config from file and print json and missing certs", + config: customConfigPath, + output: "json", + brokenCertName: brokenCertName, + }, + { + name: "fetch kubeadm config from server and print json and missing certs", + output: "json", + brokenCertName: brokenCertName, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + if tc.brokenCertName != "" { + // remove the file to simulate a missing certificate + _ = os.Remove(brokenCertPath) + } + runTestCase(t, tc) + }) + } +} diff --git a/cmd/kubeadm/app/cmd/upgrade/diff.go b/cmd/kubeadm/app/cmd/upgrade/diff.go index 4f7a4e7322d..0b2a46f311f 100644 --- a/cmd/kubeadm/app/cmd/upgrade/diff.go +++ b/cmd/kubeadm/app/cmd/upgrade/diff.go @@ -115,7 +115,7 @@ func runDiff(flags *diffFlags, args []string) error { SkipCRIDetect: true, }) } else { - var client *client.Clientset + var client client.Interface client, err = kubeconfigutil.ClientSetFromFile(flags.kubeConfigPath) if err != nil { return errors.Wrapf(err, "couldn't create a Kubernetes client from file %q", flags.kubeConfigPath) diff --git a/cmd/kubeadm/app/util/kubeconfig/kubeconfig.go b/cmd/kubeadm/app/util/kubeconfig/kubeconfig.go index 332b22279d8..17fe4dbec26 100644 --- a/cmd/kubeadm/app/util/kubeconfig/kubeconfig.go +++ b/cmd/kubeadm/app/util/kubeconfig/kubeconfig.go @@ -70,7 +70,7 @@ func CreateWithToken(serverURL, clusterName, userName string, caCert []byte, tok } // ClientSetFromFile returns a ready-to-use client from a kubeconfig file -func ClientSetFromFile(path string) (*clientset.Clientset, error) { +func ClientSetFromFile(path string) (clientset.Interface, error) { config, err := clientcmd.LoadFromFile(path) if err != nil { return nil, errors.Wrap(err, "failed to load admin kubeconfig") @@ -79,7 +79,7 @@ func ClientSetFromFile(path string) (*clientset.Clientset, error) { } // ToClientSet converts a KubeConfig object to a client -func ToClientSet(config *clientcmdapi.Config) (*clientset.Clientset, error) { +func ToClientSet(config *clientcmdapi.Config) (clientset.Interface, error) { overrides := clientcmd.ConfigOverrides{Timeout: "10s"} clientConfig, err := clientcmd.NewDefaultClientConfig(*config, &overrides).ClientConfig() if err != nil {