pax_global_header00006660000000000000000000000064147642221300014513gustar00rootroot0000000000000052 comment=c9cca2fa06451d1f9716871b3fe4bf8253fffe52 kubernetes-component-base-1b2882b/000077500000000000000000000000001476422213000171245ustar00rootroot00000000000000kubernetes-component-base-1b2882b/.github/000077500000000000000000000000001476422213000204645ustar00rootroot00000000000000kubernetes-component-base-1b2882b/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000002251476422213000242640ustar00rootroot00000000000000Sorry, we do not accept changes directly against this repository. Please see CONTRIBUTING.md for information on where and how to contribute instead. kubernetes-component-base-1b2882b/CONTRIBUTING.md000066400000000000000000000013561476422213000213620ustar00rootroot00000000000000# Contributing guidelines Do not open pull requests directly against this repository, they will be ignored. Instead, please open pull requests against [kubernetes/kubernetes](https://git.k8s.io/kubernetes/). Please follow the same [contributing guide](https://git.k8s.io/kubernetes/CONTRIBUTING.md) you would follow for any other pull request made to kubernetes/kubernetes. This repository is published from [kubernetes/kubernetes/staging/src/k8s.io/component-base](https://git.k8s.io/kubernetes/staging/src/k8s.io/component-base) by the [kubernetes publishing-bot](https://git.k8s.io/publishing-bot). Please see [Staging Directory and Publishing](https://git.k8s.io/community/contributors/devel/sig-architecture/staging.md) for more information kubernetes-component-base-1b2882b/LICENSE000066400000000000000000000261361476422213000201410ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. kubernetes-component-base-1b2882b/OWNERS000066400000000000000000000003771476422213000200730ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners approvers: - sig-architecture-approvers reviewers: - sig-architecture-approvers emeritus_approvers: - jbeda - lavalamp - luxas - mtaufen - stealthybox - sttts labels: - sig/architecture kubernetes-component-base-1b2882b/README.md000066400000000000000000000026461476422213000204130ustar00rootroot00000000000000## component-base ## Purpose Implement KEP 32: https://github.com/kubernetes/enhancements/blob/master/keps/sig-cluster-lifecycle/wgs/783-component-base/README.md The proposal is essentially about refactoring the Kubernetes core package structure in a way that all core components may share common code around: - ComponentConfig implementation - flag and command handling - HTTPS serving - delegated authn/z - logging. ## Compatibility There are *NO compatibility guarantees* for this repository, yet. It is in direct support of Kubernetes, so branches will track Kubernetes and be compatible with that repo. As we more cleanly separate the layers, we will review the compatibility guarantee. We have a goal to make this easier to use in the future. ## Where does it come from? This repository is synced from https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/component-base. Code changes are made in that location, merged into `k8s.io/kubernetes` and later synced here. ## Things you should *NOT* do 1. Directly modify any files in this repo. Those are driven from `k8s.io/kubernetes/staging/src/k8s.io/component-base`. 2. Expect compatibility. This repo is changing quickly in direct support of Kubernetes. ### OWNERS WG Component Standard is working on this refactoring process, which is happening incrementally, starting in the v1.14 cycle. SIG API Machinery and SIG Cluster Lifecycle owns the code. kubernetes-component-base-1b2882b/SECURITY_CONTACTS000066400000000000000000000010671476422213000216200ustar00rootroot00000000000000# Defined below are the security contacts for this repo. # # They are the contact point for the Product Security Committee to reach out # to for triaging and handling of incoming issues. # # The below names agree to abide by the # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) # and will be removed and replaced if they violate that agreement. # # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE # INSTRUCTIONS AT https://kubernetes.io/security/ cjcullen joelsmith liggitt luxas sttts tallclair kubernetes-component-base-1b2882b/cli/000077500000000000000000000000001476422213000176735ustar00rootroot00000000000000kubernetes-component-base-1b2882b/cli/OWNERS000066400000000000000000000004121476422213000206300ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners # Currently assigned cli to sig-cli since: # (a) its literally named "cli" # (b) flags are the bread-and-butter of cli tools. approvers: - sig-cli-maintainers reviewers: - sig-cli-reviewers labels: - sig/cli kubernetes-component-base-1b2882b/cli/flag/000077500000000000000000000000001476422213000206045ustar00rootroot00000000000000kubernetes-component-base-1b2882b/cli/flag/ciphersuites_flag.go000066400000000000000000000103311476422213000246310ustar00rootroot00000000000000/* Copyright 2017 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 flag import ( "crypto/tls" "fmt" "k8s.io/apimachinery/pkg/util/sets" ) var ( // ciphers maps strings into tls package cipher constants in // https://golang.org/pkg/crypto/tls/#pkg-constants ciphers = map[string]uint16{} insecureCiphers = map[string]uint16{} ) func init() { for _, suite := range tls.CipherSuites() { ciphers[suite.Name] = suite.ID } // keep legacy names for backward compatibility ciphers["TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305"] = tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 ciphers["TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"] = tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 for _, suite := range tls.InsecureCipherSuites() { insecureCiphers[suite.Name] = suite.ID } } // InsecureTLSCiphers returns the cipher suites implemented by crypto/tls which have // security issues. func InsecureTLSCiphers() map[string]uint16 { cipherKeys := make(map[string]uint16, len(insecureCiphers)) for k, v := range insecureCiphers { cipherKeys[k] = v } return cipherKeys } // InsecureTLSCipherNames returns a list of cipher suite names implemented by crypto/tls // which have security issues. func InsecureTLSCipherNames() []string { cipherKeys := sets.NewString() for key := range insecureCiphers { cipherKeys.Insert(key) } return cipherKeys.List() } // PreferredTLSCipherNames returns a list of cipher suite names implemented by crypto/tls. func PreferredTLSCipherNames() []string { cipherKeys := sets.NewString() for key := range ciphers { cipherKeys.Insert(key) } return cipherKeys.List() } func allCiphers() map[string]uint16 { acceptedCiphers := make(map[string]uint16, len(ciphers)+len(insecureCiphers)) for k, v := range ciphers { acceptedCiphers[k] = v } for k, v := range insecureCiphers { acceptedCiphers[k] = v } return acceptedCiphers } // TLSCipherPossibleValues returns all acceptable cipher suite names. // This is a combination of both InsecureTLSCipherNames() and PreferredTLSCipherNames(). func TLSCipherPossibleValues() []string { cipherKeys := sets.NewString() acceptedCiphers := allCiphers() for key := range acceptedCiphers { cipherKeys.Insert(key) } return cipherKeys.List() } // TLSCipherSuites returns a list of cipher suite IDs from the cipher suite names passed. func TLSCipherSuites(cipherNames []string) ([]uint16, error) { if len(cipherNames) == 0 { return nil, nil } ciphersIntSlice := make([]uint16, 0) possibleCiphers := allCiphers() for _, cipher := range cipherNames { intValue, ok := possibleCiphers[cipher] if !ok { return nil, fmt.Errorf("Cipher suite %s not supported or doesn't exist", cipher) } ciphersIntSlice = append(ciphersIntSlice, intValue) } return ciphersIntSlice, nil } var versions = map[string]uint16{ "VersionTLS10": tls.VersionTLS10, "VersionTLS11": tls.VersionTLS11, "VersionTLS12": tls.VersionTLS12, "VersionTLS13": tls.VersionTLS13, } // TLSPossibleVersions returns all acceptable values for TLS Version. func TLSPossibleVersions() []string { versionsKeys := sets.NewString() for key := range versions { versionsKeys.Insert(key) } return versionsKeys.List() } // TLSVersion returns the TLS Version ID for the version name passed. func TLSVersion(versionName string) (uint16, error) { if len(versionName) == 0 { return DefaultTLSVersion(), nil } if version, ok := versions[versionName]; ok { return version, nil } return 0, fmt.Errorf("unknown tls version %q", versionName) } // DefaultTLSVersion defines the default TLS Version. func DefaultTLSVersion() uint16 { // Can't use SSLv3 because of POODLE and BEAST // Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher // Can't use TLSv1.1 because of RC4 cipher usage return tls.VersionTLS12 } kubernetes-component-base-1b2882b/cli/flag/ciphersuites_flag_test.go000066400000000000000000000111121476422213000256660ustar00rootroot00000000000000/* Copyright 2017 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 flag import ( "crypto/tls" "reflect" "testing" ) func TestStrToUInt16(t *testing.T) { tests := []struct { flag []string expected []uint16 expected_error bool }{ { // Happy case flag: []string{"TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"}, expected: []uint16{tls.TLS_RSA_WITH_RC4_128_SHA, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA}, expected_error: false, }, { // One flag only flag: []string{"TLS_RSA_WITH_RC4_128_SHA"}, expected: []uint16{tls.TLS_RSA_WITH_RC4_128_SHA}, expected_error: false, }, { // Empty flag flag: []string{}, expected: nil, expected_error: false, }, { // Duplicated flag flag: []string{"TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_RC4_128_SHA"}, expected: []uint16{tls.TLS_RSA_WITH_RC4_128_SHA, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_RC4_128_SHA}, expected_error: false, }, { // Invalid flag flag: []string{"foo"}, expected: nil, expected_error: true, }, { // All existing cipher suites flag: []string{ "TLS_RSA_WITH_3DES_EDE_CBC_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_AES_128_GCM_SHA256", "TLS_CHACHA20_POLY1305_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_RSA_WITH_RC4_128_SHA", "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", "TLS_ECDHE_RSA_WITH_RC4_128_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", }, expected: []uint16{ tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_AES_256_CBC_SHA, tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_AES_128_GCM_SHA256, tls.TLS_CHACHA20_POLY1305_SHA256, tls.TLS_AES_256_GCM_SHA384, tls.TLS_RSA_WITH_RC4_128_SHA, tls.TLS_RSA_WITH_AES_128_CBC_SHA256, tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, }, }, } for i, test := range tests { uIntFlags, err := TLSCipherSuites(test.flag) if !reflect.DeepEqual(uIntFlags, test.expected) { t.Errorf("%d: expected %+v, got %+v", i, test.expected, uIntFlags) } if test.expected_error && err == nil { t.Errorf("%d: expecting error, got %+v", i, err) } } } kubernetes-component-base-1b2882b/cli/flag/colon_separated_multimap_string_string.go000066400000000000000000000103551476422213000311650ustar00rootroot00000000000000/* Copyright 2017 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 flag import ( "fmt" "sort" "strings" ) // ColonSeparatedMultimapStringString supports setting a map[string][]string from an encoding // that separates keys from values with ':' and separates key-value pairs with ','. // A key can be repeated multiple times, in which case the values are appended to a // slice of strings associated with that key. Items in the list associated with a given // key will appear in the order provided. // For example: `a:hello,b:again,c:world,b:beautiful` results in `{"a": ["hello"], "b": ["again", "beautiful"], "c": ["world"]}` // The first call to Set will clear the map before adding entries; subsequent calls will simply append to the map. // This makes it possible to override default values with a command-line option rather than appending to defaults, // while still allowing the distribution of key-value pairs across multiple flag invocations. // For example: `--flag "a:hello" --flag "b:again" --flag "b:beautiful" --flag "c:world"` results in `{"a": ["hello"], "b": ["again", "beautiful"], "c": ["world"]}` type ColonSeparatedMultimapStringString struct { Multimap *map[string][]string initialized bool // set to true after the first Set call allowDefaultEmptyKey bool } // NewColonSeparatedMultimapStringString takes a pointer to a map[string][]string and returns the // ColonSeparatedMultimapStringString flag parsing shim for that map. func NewColonSeparatedMultimapStringString(m *map[string][]string) *ColonSeparatedMultimapStringString { return &ColonSeparatedMultimapStringString{Multimap: m} } // NewColonSeparatedMultimapStringStringAllowDefaultEmptyKey takes a pointer to a map[string][]string and returns the // ColonSeparatedMultimapStringString flag parsing shim for that map. It allows default empty key with no colon in the flag. func NewColonSeparatedMultimapStringStringAllowDefaultEmptyKey(m *map[string][]string) *ColonSeparatedMultimapStringString { return &ColonSeparatedMultimapStringString{Multimap: m, allowDefaultEmptyKey: true} } // Set implements github.com/spf13/pflag.Value func (m *ColonSeparatedMultimapStringString) Set(value string) error { if m.Multimap == nil { return fmt.Errorf("no target (nil pointer to map[string][]string)") } if !m.initialized || *m.Multimap == nil { // clear default values, or allocate if no existing map *m.Multimap = make(map[string][]string) m.initialized = true } for _, pair := range strings.Split(value, ",") { if len(pair) == 0 { continue } kv := strings.SplitN(pair, ":", 2) var k, v string if m.allowDefaultEmptyKey && len(kv) == 1 { v = strings.TrimSpace(kv[0]) } else { if len(kv) != 2 { return fmt.Errorf("malformed pair, expect string:string") } k = strings.TrimSpace(kv[0]) v = strings.TrimSpace(kv[1]) } (*m.Multimap)[k] = append((*m.Multimap)[k], v) } return nil } // String implements github.com/spf13/pflag.Value func (m *ColonSeparatedMultimapStringString) String() string { type kv struct { k string v string } kvs := make([]kv, 0, len(*m.Multimap)) for k, vs := range *m.Multimap { for i := range vs { kvs = append(kvs, kv{k: k, v: vs[i]}) } } // stable sort by keys, order of values should be preserved sort.SliceStable(kvs, func(i, j int) bool { return kvs[i].k < kvs[j].k }) pairs := make([]string, 0, len(kvs)) for i := range kvs { pairs = append(pairs, fmt.Sprintf("%s:%s", kvs[i].k, kvs[i].v)) } return strings.Join(pairs, ",") } // Type implements github.com/spf13/pflag.Value func (m *ColonSeparatedMultimapStringString) Type() string { return "colonSeparatedMultimapStringString" } // Empty implements OmitEmpty func (m *ColonSeparatedMultimapStringString) Empty() bool { return len(*m.Multimap) == 0 } kubernetes-component-base-1b2882b/cli/flag/colon_separated_multimap_string_string_test.go000066400000000000000000000200131476422213000322140ustar00rootroot00000000000000/* Copyright 2017 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 flag import ( "reflect" "testing" ) func TestStringColonSeparatedMultimapStringString(t *testing.T) { var nilMap map[string][]string cases := []struct { desc string m *ColonSeparatedMultimapStringString expect string }{ {"nil", NewColonSeparatedMultimapStringString(&nilMap), ""}, {"empty", NewColonSeparatedMultimapStringString(&map[string][]string{}), ""}, {"empty key", NewColonSeparatedMultimapStringString( &map[string][]string{ "": {"foo"}, }), ":foo"}, {"one key", NewColonSeparatedMultimapStringString( &map[string][]string{ "one": {"foo"}, }), "one:foo"}, {"two keys", NewColonSeparatedMultimapStringString( &map[string][]string{ "one": {"foo"}, "two": {"bar"}, }), "one:foo,two:bar"}, {"two keys, multiple items in one key", NewColonSeparatedMultimapStringString( &map[string][]string{ "one": {"foo", "baz"}, "two": {"bar"}, }), "one:foo,one:baz,two:bar"}, {"three keys, multiple items in one key", NewColonSeparatedMultimapStringString( &map[string][]string{ "a": {"hello"}, "b": {"again", "beautiful"}, "c": {"world"}, }), "a:hello,b:again,b:beautiful,c:world"}, } for _, c := range cases { t.Run(c.desc, func(t *testing.T) { str := c.m.String() if c.expect != str { t.Fatalf("expect %q but got %q", c.expect, str) } }) } } func TestSetColonSeparatedMultimapStringString(t *testing.T) { var nilMap map[string][]string cases := []struct { desc string vals []string start *ColonSeparatedMultimapStringString expect *ColonSeparatedMultimapStringString err string }{ // we initialize the map with a default key that should be cleared by Set {"clears defaults", []string{""}, NewColonSeparatedMultimapStringString(&map[string][]string{"default": {}}), &ColonSeparatedMultimapStringString{ initialized: true, Multimap: &map[string][]string{}}, ""}, // make sure we still allocate for "initialized" multimaps where Multimap was initially set to a nil map {"allocates map if currently nil", []string{""}, &ColonSeparatedMultimapStringString{initialized: true, Multimap: &nilMap}, &ColonSeparatedMultimapStringString{ initialized: true, Multimap: &map[string][]string{}, }, ""}, // for most cases, we just reuse nilMap, which should be allocated by Set, and is reset before each test case {"empty", []string{""}, NewColonSeparatedMultimapStringString(&nilMap), &ColonSeparatedMultimapStringString{ initialized: true, Multimap: &map[string][]string{}}, ""}, {"empty key no colon", []string{"foo"}, NewColonSeparatedMultimapStringString(&nilMap), &ColonSeparatedMultimapStringString{ initialized: true, Multimap: &map[string][]string{ "": {"foo"}, }}, "malformed pair, expect string:string"}, {"empty key no colon allowed", []string{"foo"}, NewColonSeparatedMultimapStringStringAllowDefaultEmptyKey(&nilMap), &ColonSeparatedMultimapStringString{ initialized: true, allowDefaultEmptyKey: true, Multimap: &map[string][]string{ "": {"foo"}, }}, ""}, {"empty key", []string{":foo"}, NewColonSeparatedMultimapStringString(&nilMap), &ColonSeparatedMultimapStringString{ initialized: true, Multimap: &map[string][]string{ "": {"foo"}, }}, ""}, {"one key", []string{"one:foo"}, NewColonSeparatedMultimapStringString(&nilMap), &ColonSeparatedMultimapStringString{ initialized: true, Multimap: &map[string][]string{ "one": {"foo"}, }}, ""}, {"two keys", []string{"one:foo,two:bar"}, NewColonSeparatedMultimapStringString(&nilMap), &ColonSeparatedMultimapStringString{ initialized: true, Multimap: &map[string][]string{ "one": {"foo"}, "two": {"bar"}, }}, ""}, {"two keys with space", []string{"one:foo, two:bar"}, NewColonSeparatedMultimapStringString(&nilMap), &ColonSeparatedMultimapStringString{ initialized: true, Multimap: &map[string][]string{ "one": {"foo"}, "two": {"bar"}, }}, ""}, {"two keys, multiple items in one key", []string{"one: foo, two:bar, one:baz"}, NewColonSeparatedMultimapStringString(&nilMap), &ColonSeparatedMultimapStringString{ initialized: true, Multimap: &map[string][]string{ "one": {"foo", "baz"}, "two": {"bar"}, }}, ""}, {"three keys, multiple items in one key", []string{"a:hello,b:again,c:world,b:beautiful"}, NewColonSeparatedMultimapStringString(&nilMap), &ColonSeparatedMultimapStringString{ initialized: true, Multimap: &map[string][]string{ "a": {"hello"}, "b": {"again", "beautiful"}, "c": {"world"}, }}, ""}, {"three keys, multiple items in one key, multiple Set invocations", []string{"a:hello,b:again", "c:world", "b:beautiful"}, NewColonSeparatedMultimapStringString(&nilMap), &ColonSeparatedMultimapStringString{ initialized: true, Multimap: &map[string][]string{ "a": {"hello"}, "b": {"again", "beautiful"}, "c": {"world"}, }}, ""}, {"missing value", []string{"a"}, NewColonSeparatedMultimapStringString(&nilMap), nil, "malformed pair, expect string:string"}, {"no target", []string{"a:foo"}, NewColonSeparatedMultimapStringString(nil), nil, "no target (nil pointer to map[string][]string)"}, } for _, c := range cases { nilMap = nil t.Run(c.desc, func(t *testing.T) { var err error for _, val := range c.vals { err = c.start.Set(val) if err != nil { break } } if c.err != "" { if err == nil || err.Error() != c.err { t.Fatalf("expect error %s but got %v", c.err, err) } return } else if err != nil { t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(c.expect, c.start) { t.Fatalf("expect %#v but got %#v", c.expect, c.start) } }) } } func TestRoundTripColonSeparatedMultimapStringString(t *testing.T) { cases := []struct { desc string vals []string expect string }{ {"empty", []string{""}, ""}, {"empty key", []string{":foo"}, ":foo"}, {"one key", []string{"one:foo"}, "one:foo"}, {"two keys", []string{"one:foo,two:bar"}, "one:foo,two:bar"}, {"two keys, multiple items in one key", []string{"one:foo, two:bar, one:baz"}, "one:foo,one:baz,two:bar"}, {"three keys, multiple items in one key", []string{"a:hello,b:again,c:world,b:beautiful"}, "a:hello,b:again,b:beautiful,c:world"}, {"three keys, multiple items in one key, multiple Set invocations", []string{"a:hello,b:again", "c:world", "b:beautiful"}, "a:hello,b:again,b:beautiful,c:world"}, } for _, c := range cases { t.Run(c.desc, func(t *testing.T) { m := NewColonSeparatedMultimapStringString(&map[string][]string{}) for _, val := range c.vals { if err := m.Set(val); err != nil { t.Fatalf("unexpected error: %v", err) } } str := m.String() if c.expect != str { t.Fatalf("expect %q but got %q", c.expect, str) } }) } } func TestEmptyColonSeparatedMultimapStringString(t *testing.T) { var nilMap map[string][]string cases := []struct { desc string val *ColonSeparatedMultimapStringString expect bool }{ {"nil", NewColonSeparatedMultimapStringString(&nilMap), true}, {"empty", NewColonSeparatedMultimapStringString(&map[string][]string{}), true}, {"populated", NewColonSeparatedMultimapStringString(&map[string][]string{"foo": {}}), false}, } for _, c := range cases { t.Run(c.desc, func(t *testing.T) { result := c.val.Empty() if result != c.expect { t.Fatalf("expect %t but got %t", c.expect, result) } }) } } kubernetes-component-base-1b2882b/cli/flag/configuration_map.go000066400000000000000000000023561476422213000246450ustar00rootroot00000000000000/* Copyright 2014 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 flag import ( "fmt" "sort" "strings" ) type ConfigurationMap map[string]string func (m *ConfigurationMap) String() string { pairs := []string{} for k, v := range *m { pairs = append(pairs, fmt.Sprintf("%s=%s", k, v)) } sort.Strings(pairs) return strings.Join(pairs, ",") } func (m *ConfigurationMap) Set(value string) error { for _, s := range strings.Split(value, ",") { if len(s) == 0 { continue } arr := strings.SplitN(s, "=", 2) if len(arr) == 2 { (*m)[strings.TrimSpace(arr[0])] = strings.TrimSpace(arr[1]) } else { (*m)[strings.TrimSpace(arr[0])] = "" } } return nil } func (*ConfigurationMap) Type() string { return "mapStringString" } kubernetes-component-base-1b2882b/cli/flag/flags.go000066400000000000000000000040311476422213000222250ustar00rootroot00000000000000/* Copyright 2014 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 flag import ( goflag "flag" "strings" "github.com/spf13/pflag" "k8s.io/klog/v2" ) var underscoreWarnings = make(map[string]struct{}) // WordSepNormalizeFunc changes all flags that contain "_" separators func WordSepNormalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName { if strings.Contains(name, "_") { return pflag.NormalizedName(strings.Replace(name, "_", "-", -1)) } return pflag.NormalizedName(name) } // WarnWordSepNormalizeFunc changes and warns for flags that contain "_" separators func WarnWordSepNormalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName { if strings.Contains(name, "_") { nname := strings.Replace(name, "_", "-", -1) if _, alreadyWarned := underscoreWarnings[name]; !alreadyWarned { klog.Warningf("using an underscore in a flag name is not supported. %s has been converted to %s.", name, nname) underscoreWarnings[name] = struct{}{} } return pflag.NormalizedName(nname) } return pflag.NormalizedName(name) } // InitFlags normalizes, parses, then logs the command line flags func InitFlags() { pflag.CommandLine.SetNormalizeFunc(WordSepNormalizeFunc) pflag.CommandLine.AddGoFlagSet(goflag.CommandLine) pflag.Parse() pflag.VisitAll(func(flag *pflag.Flag) { klog.V(2).Infof("FLAG: --%s=%q", flag.Name, flag.Value) }) } // PrintFlags logs the flags in the flagset func PrintFlags(flags *pflag.FlagSet) { flags.VisitAll(func(flag *pflag.Flag) { klog.V(1).Infof("FLAG: --%s=%q", flag.Name, flag.Value) }) } kubernetes-component-base-1b2882b/cli/flag/flags_test.go000066400000000000000000000060171476422213000232720ustar00rootroot00000000000000/* Copyright 2021 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 flag import ( "bytes" "testing" "github.com/go-logr/logr" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "k8s.io/klog/v2" ) type FakeLogger struct { logr.Logger infoBuffer bytes.Buffer errorBuffer bytes.Buffer } func (logger *FakeLogger) Init(info logr.RuntimeInfo) {} func (logger *FakeLogger) Enabled(lvl int) bool { return true } func (logger *FakeLogger) Info(lvl int, msg string, keysAndValues ...interface{}) { logger.infoBuffer.WriteString(msg) } func (logger *FakeLogger) Error(err error, msg string, keysAndValues ...interface{}) { logger.errorBuffer.WriteString(msg) } func (logger *FakeLogger) WithValues(keysAndValues ...interface{}) logr.LogSink { return logger } func (logger *FakeLogger) WithName(name string) logr.LogSink { return logger } var _ logr.LogSink = &FakeLogger{} func TestWordSepNormalizeFunc(t *testing.T) { cases := []struct { flagName string expectedFlagName string }{ { flagName: "foo", expectedFlagName: "foo", }, { flagName: "foo-bar", expectedFlagName: "foo-bar", }, { flagName: "foo_bar", expectedFlagName: "foo-bar", }, } for _, tc := range cases { t.Run(tc.flagName, func(t *testing.T) { fakeLogger := &FakeLogger{} klog.SetLogger(logr.New(fakeLogger)) result := WordSepNormalizeFunc(nil, tc.flagName) assert.Equal(t, pflag.NormalizedName(tc.expectedFlagName), result) assert.Equal(t, "", fakeLogger.infoBuffer.String()) assert.Equal(t, "", fakeLogger.errorBuffer.String()) }) } } func TestWarnWordSepNormalizeFunc(t *testing.T) { cases := []struct { flagName string expectedFlagName string expectedWarning string }{ { flagName: "foo", expectedFlagName: "foo", expectedWarning: "", }, { flagName: "foo-bar", expectedFlagName: "foo-bar", expectedWarning: "", }, { flagName: "foo_bar", expectedFlagName: "foo-bar", expectedWarning: "using an underscore in a flag name is not supported. foo_bar has been converted to foo-bar.", }, } for _, tc := range cases { t.Run(tc.flagName, func(t *testing.T) { fakeLogger := &FakeLogger{} klog.SetLogger(logr.New(fakeLogger)) result := WarnWordSepNormalizeFunc(nil, tc.flagName) assert.Equal(t, pflag.NormalizedName(tc.expectedFlagName), result) assert.Equal(t, tc.expectedWarning, fakeLogger.infoBuffer.String()) assert.Equal(t, "", fakeLogger.errorBuffer.String()) }) } } kubernetes-component-base-1b2882b/cli/flag/langle_separated_map_string_string.go000066400000000000000000000047771476422213000302550ustar00rootroot00000000000000/* Copyright 2017 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 flag import ( "fmt" "sort" "strings" ) // LangleSeparatedMapStringString can be set from the command line with the format `--flag "string 0 { s = s + ":" + strings.Join(nkc.Names, ",") } return s } func (nkc *NamedCertKey) Set(value string) error { cs := strings.SplitN(value, ":", 2) var keycert string if len(cs) == 2 { var names string keycert, names = strings.TrimSpace(cs[0]), strings.TrimSpace(cs[1]) if names == "" { return errors.New("empty names list is not allowed") } nkc.Names = nil for _, name := range strings.Split(names, ",") { nkc.Names = append(nkc.Names, strings.TrimSpace(name)) } } else { nkc.Names = nil keycert = strings.TrimSpace(cs[0]) } cs = strings.Split(keycert, ",") if len(cs) != 2 { return errors.New("expected comma separated certificate and key file paths") } nkc.CertFile = strings.TrimSpace(cs[0]) nkc.KeyFile = strings.TrimSpace(cs[1]) return nil } func (*NamedCertKey) Type() string { return "namedCertKey" } // NamedCertKeyArray is a flag value parsing NamedCertKeys, each passed with its own // flag instance (in contrast to comma separated slices). type NamedCertKeyArray struct { value *[]NamedCertKey changed bool } var _ flag.Value = &NamedCertKeyArray{} // NewNamedKeyCertArray creates a new NamedCertKeyArray with the internal value // pointing to p. func NewNamedCertKeyArray(p *[]NamedCertKey) *NamedCertKeyArray { return &NamedCertKeyArray{ value: p, } } func (a *NamedCertKeyArray) Set(val string) error { nkc := NamedCertKey{} err := nkc.Set(val) if err != nil { return err } if !a.changed { *a.value = []NamedCertKey{nkc} a.changed = true } else { *a.value = append(*a.value, nkc) } return nil } func (a *NamedCertKeyArray) Type() string { return "namedCertKey" } func (a *NamedCertKeyArray) String() string { nkcs := make([]string, 0, len(*a.value)) for i := range *a.value { nkcs = append(nkcs, (*a.value)[i].String()) } return "[" + strings.Join(nkcs, ";") + "]" } kubernetes-component-base-1b2882b/cli/flag/namedcertkey_flag_test.go000066400000000000000000000064461476422213000256500ustar00rootroot00000000000000/* Copyright 2016 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 flag import ( "fmt" "reflect" "strings" "testing" "github.com/spf13/pflag" ) func TestNamedCertKeyArrayFlag(t *testing.T) { tests := []struct { args []string def []NamedCertKey expected []NamedCertKey parseError string }{ { args: []string{}, expected: nil, }, { args: []string{"foo.crt,foo.key"}, expected: []NamedCertKey{{ KeyFile: "foo.key", CertFile: "foo.crt", }}, }, { args: []string{" foo.crt , foo.key "}, expected: []NamedCertKey{{ KeyFile: "foo.key", CertFile: "foo.crt", }}, }, { args: []string{"foo.crt,foo.key:abc"}, expected: []NamedCertKey{{ KeyFile: "foo.key", CertFile: "foo.crt", Names: []string{"abc"}, }}, }, { args: []string{"foo.crt,foo.key: abc "}, expected: []NamedCertKey{{ KeyFile: "foo.key", CertFile: "foo.crt", Names: []string{"abc"}, }}, }, { args: []string{"foo.crt,foo.key:"}, parseError: "empty names list is not allowed", }, { args: []string{""}, parseError: "expected comma separated certificate and key file paths", }, { args: []string{" "}, parseError: "expected comma separated certificate and key file paths", }, { args: []string{"a,b,c"}, parseError: "expected comma separated certificate and key file paths", }, { args: []string{"foo.crt,foo.key:abc,def,ghi"}, expected: []NamedCertKey{{ KeyFile: "foo.key", CertFile: "foo.crt", Names: []string{"abc", "def", "ghi"}, }}, }, { args: []string{"foo.crt,foo.key:*.*.*"}, expected: []NamedCertKey{{ KeyFile: "foo.key", CertFile: "foo.crt", Names: []string{"*.*.*"}, }}, }, { args: []string{"foo.crt,foo.key", "bar.crt,bar.key"}, expected: []NamedCertKey{{ KeyFile: "foo.key", CertFile: "foo.crt", }, { KeyFile: "bar.key", CertFile: "bar.crt", }}, }, } for i, test := range tests { fs := pflag.NewFlagSet("testNamedCertKeyArray", pflag.ContinueOnError) var nkcs []NamedCertKey nkcs = append(nkcs, test.def...) fs.Var(NewNamedCertKeyArray(&nkcs), "tls-sni-cert-key", "usage") args := []string{} for _, a := range test.args { args = append(args, fmt.Sprintf("--tls-sni-cert-key=%s", a)) } err := fs.Parse(args) if test.parseError != "" { if err == nil { t.Errorf("%d: expected error %q, got nil", i, test.parseError) } else if !strings.Contains(err.Error(), test.parseError) { t.Errorf("%d: expected error %q, got %q", i, test.parseError, err) } } else if err != nil { t.Errorf("%d: expected nil error, got %v", i, err) } if !reflect.DeepEqual(nkcs, test.expected) { t.Errorf("%d: expected %+v, got %+v", i, test.expected, nkcs) } } } kubernetes-component-base-1b2882b/cli/flag/noop.go000066400000000000000000000016541476422213000221140ustar00rootroot00000000000000/* Copyright 2018 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 flag import ( goflag "flag" "github.com/spf13/pflag" ) // NoOp implements goflag.Value and plfag.Value, // but has a noop Set implementation type NoOp struct{} var _ goflag.Value = NoOp{} var _ pflag.Value = NoOp{} func (NoOp) String() string { return "" } func (NoOp) Set(val string) error { return nil } func (NoOp) Type() string { return "NoOp" } kubernetes-component-base-1b2882b/cli/flag/omitempty.go000066400000000000000000000015331476422213000231640ustar00rootroot00000000000000/* Copyright 2017 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 flag // OmitEmpty is an interface for flags to report whether their underlying value // is "empty." If a flag implements OmitEmpty and returns true for a call to Empty(), // it is assumed that flag may be omitted from the command line. type OmitEmpty interface { Empty() bool } kubernetes-component-base-1b2882b/cli/flag/sectioned.go000066400000000000000000000061731476422213000231170ustar00rootroot00000000000000/* Copyright 2018 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 flag import ( "bytes" "fmt" "io" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" ) const ( usageFmt = "Usage:\n %s\n" ) // NamedFlagSets stores named flag sets in the order of calling FlagSet. type NamedFlagSets struct { // Order is an ordered list of flag set names. Order []string // FlagSets stores the flag sets by name. FlagSets map[string]*pflag.FlagSet // NormalizeNameFunc is the normalize function which used to initialize FlagSets created by NamedFlagSets. NormalizeNameFunc func(f *pflag.FlagSet, name string) pflag.NormalizedName } // FlagSet returns the flag set with the given name and adds it to the // ordered name list if it is not in there yet. func (nfs *NamedFlagSets) FlagSet(name string) *pflag.FlagSet { if nfs.FlagSets == nil { nfs.FlagSets = map[string]*pflag.FlagSet{} } if _, ok := nfs.FlagSets[name]; !ok { flagSet := pflag.NewFlagSet(name, pflag.ExitOnError) flagSet.SetNormalizeFunc(pflag.CommandLine.GetNormalizeFunc()) if nfs.NormalizeNameFunc != nil { flagSet.SetNormalizeFunc(nfs.NormalizeNameFunc) } nfs.FlagSets[name] = flagSet nfs.Order = append(nfs.Order, name) } return nfs.FlagSets[name] } // PrintSections prints the given names flag sets in sections, with the maximal given column number. // If cols is zero, lines are not wrapped. func PrintSections(w io.Writer, fss NamedFlagSets, cols int) { for _, name := range fss.Order { fs := fss.FlagSets[name] if !fs.HasFlags() { continue } wideFS := pflag.NewFlagSet("", pflag.ExitOnError) wideFS.AddFlagSet(fs) var zzz string if cols > 24 { zzz = strings.Repeat("z", cols-24) wideFS.Int(zzz, 0, strings.Repeat("z", cols-24)) } var buf bytes.Buffer fmt.Fprintf(&buf, "\n%s flags:\n\n%s", strings.ToUpper(name[:1])+name[1:], wideFS.FlagUsagesWrapped(cols)) if cols > 24 { i := strings.Index(buf.String(), zzz) lines := strings.Split(buf.String()[:i], "\n") fmt.Fprint(w, strings.Join(lines[:len(lines)-1], "\n")) fmt.Fprintln(w) } else { fmt.Fprint(w, buf.String()) } } } // SetUsageAndHelpFunc set both usage and help function. // Print the flag sets we need instead of all of them. func SetUsageAndHelpFunc(cmd *cobra.Command, fss NamedFlagSets, cols int) { cmd.SetUsageFunc(func(cmd *cobra.Command) error { fmt.Fprintf(cmd.OutOrStderr(), usageFmt, cmd.UseLine()) PrintSections(cmd.OutOrStderr(), fss, cols) return nil }) cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n"+usageFmt, cmd.Long, cmd.UseLine()) PrintSections(cmd.OutOrStdout(), fss, cols) }) } kubernetes-component-base-1b2882b/cli/flag/string_flag.go000066400000000000000000000024731476422213000234400ustar00rootroot00000000000000/* Copyright 2014 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 flag // StringFlag is a string flag compatible with flags and pflags that keeps track of whether it had a value supplied or not. type StringFlag struct { // If Set has been invoked this value is true provided bool // The exact value provided on the flag value string } func NewStringFlag(defaultVal string) StringFlag { return StringFlag{value: defaultVal} } func (f *StringFlag) Default(value string) { f.value = value } func (f StringFlag) String() string { return f.value } func (f StringFlag) Value() string { return f.value } func (f *StringFlag) Set(value string) error { f.value = value f.provided = true return nil } func (f StringFlag) Provided() bool { return f.provided } func (f *StringFlag) Type() string { return "string" } kubernetes-component-base-1b2882b/cli/flag/string_slice_flag.go000066400000000000000000000026231476422213000246140ustar00rootroot00000000000000/* Copyright 2021 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 flag import ( goflag "flag" "fmt" "strings" "github.com/spf13/pflag" ) // StringSlice implements goflag.Value and plfag.Value, // and allows set to be invoked repeatedly to accumulate values. type StringSlice struct { value *[]string changed bool } func NewStringSlice(s *[]string) *StringSlice { return &StringSlice{value: s} } var _ goflag.Value = &StringSlice{} var _ pflag.Value = &StringSlice{} func (s *StringSlice) String() string { if s == nil || s.value == nil { return "" } return strings.Join(*s.value, " ") } func (s *StringSlice) Set(val string) error { if s.value == nil { return fmt.Errorf("no target (nil pointer to []string)") } if !s.changed { *s.value = make([]string, 0) } *s.value = append(*s.value, val) s.changed = true return nil } func (StringSlice) Type() string { return "sliceString" } kubernetes-component-base-1b2882b/cli/flag/string_slice_flag_test.go000066400000000000000000000042331476422213000256520ustar00rootroot00000000000000/* Copyright 2021 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 flag import ( "fmt" "reflect" "strings" "testing" "github.com/spf13/pflag" ) func TestStringSlice(t *testing.T) { tests := []struct { args []string def []string expected []string parseError string changed bool }{ { args: []string{}, expected: nil, }, { args: []string{"a"}, expected: []string{"a"}, changed: true, }, { args: []string{"a", "b"}, expected: []string{"a", "b"}, changed: true, }, { def: []string{"a"}, args: []string{"a", "b"}, expected: []string{"a", "b"}, changed: true, }, { def: []string{"a", "b"}, args: []string{"a", "b"}, expected: []string{"a", "b"}, changed: true, }, } for i, test := range tests { fs := pflag.NewFlagSet("testStringSlice", pflag.ContinueOnError) var s []string s = append(s, test.def...) v := NewStringSlice(&s) fs.Var(v, "slice", "usage") args := []string{} for _, a := range test.args { args = append(args, fmt.Sprintf("--slice=%s", a)) } err := fs.Parse(args) if test.parseError != "" { if err == nil { t.Errorf("%d: expected error %q, got nil", i, test.parseError) } else if !strings.Contains(err.Error(), test.parseError) { t.Errorf("%d: expected error %q, got %q", i, test.parseError, err) } } else if err != nil { t.Errorf("%d: expected nil error, got %v", i, err) } if !reflect.DeepEqual(s, test.expected) { t.Errorf("%d: expected %+v, got %+v", i, test.expected, s) } if v.changed != test.changed { t.Errorf("%d: expected %t got %t", i, test.changed, v.changed) } } } kubernetes-component-base-1b2882b/cli/flag/tristate.go000066400000000000000000000027611476422213000230000ustar00rootroot00000000000000/* Copyright 2014 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 flag import ( "fmt" "strconv" ) // Tristate is a flag compatible with flags and pflags that // keeps track of whether it had a value supplied or not. type Tristate int const ( Unset Tristate = iota // 0 True False ) func (f *Tristate) Default(value bool) { *f = triFromBool(value) } func (f Tristate) String() string { b := boolFromTri(f) return fmt.Sprintf("%t", b) } func (f Tristate) Value() bool { b := boolFromTri(f) return b } func (f *Tristate) Set(value string) error { boolVal, err := strconv.ParseBool(value) if err != nil { return err } *f = triFromBool(boolVal) return nil } func (f Tristate) Provided() bool { if f != Unset { return true } return false } func (f *Tristate) Type() string { return "tristate" } func boolFromTri(t Tristate) bool { if t == True { return true } else { return false } } func triFromBool(b bool) Tristate { if b { return True } else { return False } } kubernetes-component-base-1b2882b/cli/globalflag/000077500000000000000000000000001476422213000217655ustar00rootroot00000000000000kubernetes-component-base-1b2882b/cli/globalflag/globalflags.go000066400000000000000000000034371476422213000246000ustar00rootroot00000000000000/* Copyright 2018 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 globalflag import ( "flag" "fmt" "github.com/spf13/pflag" "k8s.io/component-base/logs" ) // AddGlobalFlags explicitly registers flags that libraries (klog, verflag, etc.) register // against the global flagsets from "flag" and "k8s.io/klog/v2". // We do this in order to prevent unwanted flags from leaking into the component's flagset. // // k8s.io/component-base/logs.SkipLoggingConfigurationFlags must be used as // option when the program also uses a LoggingConfiguration struct for // configuring logging. Then only flags not covered by that get added. func AddGlobalFlags(fs *pflag.FlagSet, name string, opts ...logs.Option) { logs.AddFlags(fs, opts...) fs.BoolP("help", "h", false, fmt.Sprintf("help for %s", name)) } // Register adds a flag to local that targets the Value associated with the Flag named globalName in flag.CommandLine. func Register(local *pflag.FlagSet, globalName string) { if f := flag.CommandLine.Lookup(globalName); f != nil { pflagFlag := pflag.PFlagFromGoFlag(f) normalizeFunc := local.GetNormalizeFunc() pflagFlag.Name = string(normalizeFunc(local, pflagFlag.Name)) local.AddFlag(pflagFlag) } else { panic(fmt.Sprintf("failed to find flag in global flagset (flag): %s", globalName)) } } kubernetes-component-base-1b2882b/cli/globalflag/globalflags_test.go000066400000000000000000000043541476422213000256360ustar00rootroot00000000000000/* Copyright 2018 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 globalflag import ( "flag" "reflect" "sort" "strings" "testing" "github.com/spf13/pflag" cliflag "k8s.io/component-base/cli/flag" "k8s.io/component-base/logs" ) func TestAddGlobalFlags(t *testing.T) { namedFlagSets := &cliflag.NamedFlagSets{} nfs := namedFlagSets.FlagSet("global") nfs.SetNormalizeFunc(cliflag.WordSepNormalizeFunc) AddGlobalFlags(nfs, "test-cmd") actualFlag := []string{} nfs.VisitAll(func(flag *pflag.Flag) { actualFlag = append(actualFlag, flag.Name) }) // Get all flags from flags.CommandLine, except flag `test.*`. wantedFlag := []string{"help"} pflag.CommandLine.AddGoFlagSet(flag.CommandLine) logs.AddFlags(pflag.CommandLine) normalizeFunc := nfs.GetNormalizeFunc() pflag.VisitAll(func(flag *pflag.Flag) { if !strings.Contains(flag.Name, "test.") { wantedFlag = append(wantedFlag, string(normalizeFunc(nfs, flag.Name))) } }) sort.Strings(wantedFlag) if !reflect.DeepEqual(wantedFlag, actualFlag) { t.Errorf("[Default]: expected %+v, got %+v", wantedFlag, actualFlag) } tests := []struct { expectedFlag []string matchExpected bool }{ { // Happy case expectedFlag: []string{"help", "log-flush-frequency", "v", "vmodule"}, matchExpected: false, }, { // Missing flag expectedFlag: []string{"logtostderr", "log-dir"}, matchExpected: true, }, { // Empty flag expectedFlag: []string{}, matchExpected: true, }, { // Invalid flag expectedFlag: []string{"foo"}, matchExpected: true, }, } for i, test := range tests { if reflect.DeepEqual(test.expectedFlag, actualFlag) == test.matchExpected { t.Errorf("[%d]: expected %+v, got %+v", i, test.expectedFlag, actualFlag) } } } kubernetes-component-base-1b2882b/cli/run.go000066400000000000000000000115151476422213000210310ustar00rootroot00000000000000/* Copyright 2021 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 cli import ( "fmt" "os" "github.com/spf13/cobra" cliflag "k8s.io/component-base/cli/flag" "k8s.io/component-base/logs" "k8s.io/klog/v2" ) // Run provides the common boilerplate code around executing a cobra command. // For example, it ensures that logging is set up properly. Logging // flags get added to the command line if not added already. Flags get normalized // so that help texts show them with hyphens. Underscores are accepted // as alternative for the command parameters. // // Run tries to be smart about how to print errors that are returned by the // command: before logging is known to be set up, it prints them as plain text // to stderr. This covers command line flag parse errors and unknown commands. // Afterwards it logs them. This covers runtime errors. // // Commands like kubectl where logging is not normally part of the runtime output // should use RunNoErrOutput instead and deal with the returned error themselves. func Run(cmd *cobra.Command) int { if logsInitialized, err := run(cmd); err != nil { // If the error is about flag parsing, then printing that error // with the decoration that klog would add ("E0923 // 23:02:03.219216 4168816 run.go:61] unknown shorthand flag") // is less readable. Using klog.Fatal is even worse because it // dumps a stack trace that isn't about the error. // // But if it is some other error encountered at runtime, then // we want to log it as error, at least in most commands because // their output is a log event stream. // // We can distinguish these two cases depending on whether // we got to logs.InitLogs() above. // // This heuristic might be problematic for command line // tools like kubectl where the output is carefully controlled // and not a log by default. They should use RunNoErrOutput // instead. // // The usage of klog is problematic also because we don't know // whether the command has managed to configure it. This cannot // be checked right now, but may become possible when the early // logging proposal from // https://github.com/kubernetes/enhancements/pull/3078 // ("contextual logging") is implemented. if !logsInitialized { fmt.Fprintf(os.Stderr, "Error: %v\n", err) } else { klog.ErrorS(err, "command failed") } return 1 } return 0 } // RunNoErrOutput is a version of Run which returns the cobra command error // instead of printing it. func RunNoErrOutput(cmd *cobra.Command) error { _, err := run(cmd) return err } func run(cmd *cobra.Command) (logsInitialized bool, err error) { defer logs.FlushLogs() cmd.SetGlobalNormalizationFunc(cliflag.WordSepNormalizeFunc) // When error printing is enabled for the Cobra command, a flag parse // error gets printed first, then optionally the often long usage // text. This is very unreadable in a console because the last few // lines that will be visible on screen don't include the error. // // The recommendation from #sig-cli was to print the usage text, then // the error. We implement this consistently for all commands here. // However, we don't want to print the usage text when command // execution fails for other reasons than parsing. We detect this via // the FlagParseError callback. // // Some commands, like kubectl, already deal with this themselves. // We don't change the behavior for those. if !cmd.SilenceUsage { cmd.SilenceUsage = true cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error { // Re-enable usage printing. c.SilenceUsage = false return err }) } // In all cases error printing is done below. cmd.SilenceErrors = true // This is idempotent. logs.AddFlags(cmd.PersistentFlags()) // Inject logs.InitLogs after command line parsing into one of the // PersistentPre* functions. switch { case cmd.PersistentPreRun != nil: pre := cmd.PersistentPreRun cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { logs.InitLogs() logsInitialized = true pre(cmd, args) } case cmd.PersistentPreRunE != nil: pre := cmd.PersistentPreRunE cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { logs.InitLogs() logsInitialized = true return pre(cmd, args) } default: cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { logs.InitLogs() logsInitialized = true } } err = cmd.Execute() return } kubernetes-component-base-1b2882b/code-of-conduct.md000066400000000000000000000002241476422213000224150ustar00rootroot00000000000000# Kubernetes Community Code of Conduct Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) kubernetes-component-base-1b2882b/codec/000077500000000000000000000000001476422213000202015ustar00rootroot00000000000000kubernetes-component-base-1b2882b/codec/OWNERS000066400000000000000000000004351476422213000211430ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners # Currently assigned codec to sig-api-machinery since its used for encoding/ # decoding KRM style manifests. approvers: - sig-api-machinery-api-approvers reviewers: - sig-api-machinery-api-reviewers labels: - sig/api-machinery kubernetes-component-base-1b2882b/codec/codec.go000066400000000000000000000026061476422213000216110ustar00rootroot00000000000000/* Copyright 2019 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 codec import ( "fmt" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" ) // NewLenientSchemeAndCodecs constructs a CodecFactory with strict decoding // disabled, that has only the Schemes registered into it which are passed // and added via AddToScheme functions. This can be used to skip strict decoding // a specific version only. func NewLenientSchemeAndCodecs(addToSchemeFns ...func(s *runtime.Scheme) error) (*runtime.Scheme, *serializer.CodecFactory, error) { lenientScheme := runtime.NewScheme() for _, s := range addToSchemeFns { if err := s(lenientScheme); err != nil { return nil, nil, fmt.Errorf("unable to add API to lenient scheme: %v", err) } } lenientCodecs := serializer.NewCodecFactory(lenientScheme, serializer.DisableStrict) return lenientScheme, &lenientCodecs, nil } kubernetes-component-base-1b2882b/config/000077500000000000000000000000001476422213000203715ustar00rootroot00000000000000kubernetes-component-base-1b2882b/config/OWNERS000066400000000000000000000004101476422213000213240ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners # Disable inheritance as this is an api owners file options: no_parent_owners: true approvers: - api-approvers reviewers: - api-reviewers labels: - kind/api-change - sig/api-machinery - sig/scheduling kubernetes-component-base-1b2882b/config/doc.go000066400000000000000000000012211476422213000214610ustar00rootroot00000000000000/* Copyright 2018 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. */ // +k8s:deepcopy-gen=package package config // import "k8s.io/component-base/config" kubernetes-component-base-1b2882b/config/options/000077500000000000000000000000001476422213000220645ustar00rootroot00000000000000kubernetes-component-base-1b2882b/config/options/leaderelectionconfig.go000066400000000000000000000051701476422213000265630ustar00rootroot00000000000000/* Copyright 2017 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 options import ( "github.com/spf13/pflag" "k8s.io/component-base/config" ) // BindLeaderElectionFlags binds the LeaderElectionConfiguration struct fields to a flagset func BindLeaderElectionFlags(l *config.LeaderElectionConfiguration, fs *pflag.FlagSet) { fs.BoolVar(&l.LeaderElect, "leader-elect", l.LeaderElect, ""+ "Start a leader election client and gain leadership before "+ "executing the main loop. Enable this when running replicated "+ "components for high availability.") fs.DurationVar(&l.LeaseDuration.Duration, "leader-elect-lease-duration", l.LeaseDuration.Duration, ""+ "The duration that non-leader candidates will wait after observing a leadership "+ "renewal until attempting to acquire leadership of a led but unrenewed leader "+ "slot. This is effectively the maximum duration that a leader can be stopped "+ "before it is replaced by another candidate. This is only applicable if leader "+ "election is enabled.") fs.DurationVar(&l.RenewDeadline.Duration, "leader-elect-renew-deadline", l.RenewDeadline.Duration, ""+ "The interval between attempts by the acting master to renew a leadership slot "+ "before it stops leading. This must be less than the lease duration. "+ "This is only applicable if leader election is enabled.") fs.DurationVar(&l.RetryPeriod.Duration, "leader-elect-retry-period", l.RetryPeriod.Duration, ""+ "The duration the clients should wait between attempting acquisition and renewal "+ "of a leadership. This is only applicable if leader election is enabled.") fs.StringVar(&l.ResourceLock, "leader-elect-resource-lock", l.ResourceLock, ""+ "The type of resource object that is used for locking during "+ "leader election. Supported options are 'leases'.") fs.StringVar(&l.ResourceName, "leader-elect-resource-name", l.ResourceName, ""+ "The name of resource object that is used for locking during "+ "leader election.") fs.StringVar(&l.ResourceNamespace, "leader-elect-resource-namespace", l.ResourceNamespace, ""+ "The namespace of resource object that is used for locking during "+ "leader election.") } kubernetes-component-base-1b2882b/config/testing/000077500000000000000000000000001476422213000220465ustar00rootroot00000000000000kubernetes-component-base-1b2882b/config/testing/apigroup.go000066400000000000000000000167371476422213000242410ustar00rootroot00000000000000/* Copyright 2019 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 testing import ( "fmt" "reflect" "regexp" "strings" apinamingtest "k8s.io/apimachinery/pkg/api/apitesting/naming" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" ) // APIVersionRegexp is the regular expression that matches with valid apiversion var APIVersionRegexp = regexp.MustCompile(`^v\d+((alpha|beta){1}\d+)?$`) // ComponentConfigPackage is used in APIGroup Testing type ComponentConfigPackage struct { ComponentName string GroupName string SchemeGroupVersion schema.GroupVersion AddToScheme func(*runtime.Scheme) error SkipTests sets.String AllowedTags map[reflect.Type]bool AllowedNonstandardJSONNames map[reflect.Type]string } type testingFunc func(*runtime.Scheme, *ComponentConfigPackage) error const ( verifyTagNaming = "verifyTagNaming" verifyGroupNameSuffix = "verifyGroupNameSuffix" verifyGroupNameMatch = "verifyGroupNameMatch" verifyCorrectGroupName = "verifyCorrectGroupName" verifyComponentConfigKindExists = "verifyComponentConfigKindExists" verifyExternalAPIVersion = "verifyExternalAPIVersion" verifyInternalAPIVersion = "verifyInternalAPIVersion" ) var testingFuncs = map[string]testingFunc{ verifyTagNaming: verifyTagNamingFunc, verifyGroupNameSuffix: verifyGroupNameSuffixFunc, verifyGroupNameMatch: verifyGroupNameMatchFunc, verifyCorrectGroupName: verifyCorrectGroupNameFunc, } // VerifyExternalTypePackage tests if external component config package is defined correctly // Test tag naming (json name should match Go name) // Test that GroupName has the k8s.io suffix // Test that GroupName == SchemeGroupVersion.GroupName // Test that the API version follows the right pattern and isn't internal // Test that addKnownTypes and AddToScheme registers at least one type and doesn't error // Test that the GroupName is named correctly (based on ComponentName), and there is a {Component}Configuration kind in the scheme func VerifyExternalTypePackage(pkginfo *ComponentConfigPackage) error { scheme, err := setup(pkginfo) if err != nil { return fmt.Errorf("test setup error: %v", err) } extraFns := map[string]testingFunc{ verifyExternalAPIVersion: verifyExternalAPIVersionFunc, } return runFuncs(scheme, pkginfo, extraFns) } // VerifyInternalTypePackage tests if internal component config package is defined correctly // Test tag naming (no tags allowed) // Test that GroupName has the k8s.io suffix // Test that GroupName == SchemeGroupVersion.GroupName // API version should be internal // Test that addKnownTypes and AddToScheme registers at least one type and doesn't error // Test that the GroupName is named correctly (based on ComponentName), and there is a {Component}Configuration kind in the scheme func VerifyInternalTypePackage(pkginfo *ComponentConfigPackage) error { scheme, err := setup(pkginfo) if err != nil { return fmt.Errorf("test setup error: %v", err) } extraFns := map[string]testingFunc{ verifyInternalAPIVersion: verifyInternalAPIVersionFunc, verifyComponentConfigKindExists: verifyComponentConfigKindExistsFunc, } return runFuncs(scheme, pkginfo, extraFns) } func setup(pkginfo *ComponentConfigPackage) (*runtime.Scheme, error) { if len(pkginfo.ComponentName) == 0 || len(pkginfo.GroupName) == 0 || pkginfo.SchemeGroupVersion.Empty() || pkginfo.AddToScheme == nil { return nil, fmt.Errorf("invalid argument: not all parameters were passed correctly to the function") } scheme := runtime.NewScheme() if err := pkginfo.AddToScheme(scheme); err != nil { return nil, fmt.Errorf("AddToScheme must not return an error: %v", err) } if len(scheme.AllKnownTypes()) == 0 { return nil, fmt.Errorf("AddToScheme doesn't register any type") } return scheme, nil } func runFuncs(scheme *runtime.Scheme, pkginfo *ComponentConfigPackage, extraFns map[string]testingFunc) error { verifyFns := []testingFunc{} for name, fn := range testingFuncs { if pkginfo.SkipTests.Has(name) { continue } verifyFns = append(verifyFns, fn) } for name, fn := range extraFns { if pkginfo.SkipTests.Has(name) { continue } verifyFns = append(verifyFns, fn) } errs := []error{} for _, fn := range verifyFns { if err := fn(scheme, pkginfo); err != nil { errs = append(errs, err) } } return errors.NewAggregate(errs) } func verifyTagNamingFunc(scheme *runtime.Scheme, pkginfo *ComponentConfigPackage) error { return apinamingtest.VerifyTagNaming(scheme, pkginfo.AllowedTags, pkginfo.AllowedNonstandardJSONNames) } func verifyGroupNameSuffixFunc(scheme *runtime.Scheme, _ *ComponentConfigPackage) error { return apinamingtest.VerifyGroupNames(scheme, sets.NewString()) } func verifyGroupNameMatchFunc(_ *runtime.Scheme, pkginfo *ComponentConfigPackage) error { if pkginfo.GroupName != pkginfo.SchemeGroupVersion.Group { return fmt.Errorf("GroupName must be equal to SchemeGroupVersion.Group, GroupName: %v,SchemeGroupVersion.Group: %v", pkginfo.GroupName, pkginfo.SchemeGroupVersion.Group) } return nil } func verifyCorrectGroupNameFunc(_ *runtime.Scheme, pkginfo *ComponentConfigPackage) error { desiredGroupName := fmt.Sprintf("%s.config.k8s.io", lowercaseWithoutDashes(pkginfo.ComponentName)) if pkginfo.SchemeGroupVersion.Group != desiredGroupName { return fmt.Errorf("got GroupName %q, want %q", pkginfo.SchemeGroupVersion.Group, desiredGroupName) } return nil } func verifyComponentConfigKindExistsFunc(scheme *runtime.Scheme, pkginfo *ComponentConfigPackage) error { expectedKind := fmt.Sprintf("%sConfiguration", dashesToCapitalCase(pkginfo.ComponentName)) expectedGVK := pkginfo.SchemeGroupVersion.WithKind(expectedKind) if !scheme.Recognizes(expectedGVK) { registeredKinds := sets.NewString() for gvk := range scheme.AllKnownTypes() { registeredKinds.Insert(gvk.Kind) } return fmt.Errorf("Kind %s not registered in the scheme, registered kinds are %v", expectedKind, registeredKinds.List()) } return nil } func verifyExternalAPIVersionFunc(_ *runtime.Scheme, pkginfo *ComponentConfigPackage) error { if !APIVersionRegexp.MatchString(pkginfo.SchemeGroupVersion.Version) { return fmt.Errorf("invalid API version %q, must match %q", pkginfo.SchemeGroupVersion.Version, APIVersionRegexp.String()) } return nil } func verifyInternalAPIVersionFunc(_ *runtime.Scheme, pkginfo *ComponentConfigPackage) error { if pkginfo.SchemeGroupVersion.Version != runtime.APIVersionInternal { return fmt.Errorf("internal API version must be %q, got %q", runtime.APIVersionInternal, pkginfo.SchemeGroupVersion.Version) } return nil } func lowercaseWithoutDashes(str string) string { return strings.Replace(strings.ToLower(str), "-", "", -1) } func dashesToCapitalCase(str string) string { segments := strings.Split(str, "-") result := "" for _, segment := range segments { result += strings.Title(segment) } return result } kubernetes-component-base-1b2882b/config/testing/apigroup_test.go000066400000000000000000000031221476422213000252600ustar00rootroot00000000000000/* Copyright 2019 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 testing import ( "testing" ) func TestAPIVersionRegexp(t *testing.T) { testCases := []struct { name string apiversion string expected bool }{ { name: "v1", apiversion: "v1", expected: true, }, { name: "v1alpha1", apiversion: "v1alpha1", expected: true, }, { name: "v1beta1", apiversion: "v1beta1", expected: true, }, { name: "doesn't start with v", apiversion: "beta1", expected: false, }, { name: "doesn't end with digit", apiversion: "v1alpha", expected: false, }, { name: "doesn't have digit after v", apiversion: "valpha1", expected: false, }, { name: "both alpha beta", apiversion: "v1alpha1beta1", expected: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actual := APIVersionRegexp.MatchString(tc.apiversion) if actual != tc.expected { t.Errorf("APIVersionRegexp expected %v, got %v", tc.expected, actual) } }) } } kubernetes-component-base-1b2882b/config/testing/defaulting.go000066400000000000000000000033351476422213000245230ustar00rootroot00000000000000/* Copyright 2019 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 testing import ( "fmt" "path/filepath" "testing" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" ) // DefaultingTest run defaulting tests for given scheme func DefaultingTest(t *testing.T, scheme *runtime.Scheme, codecs serializer.CodecFactory) { cases := GetDefaultingTestCases(t, scheme, codecs) RunTestsOnYAMLData(t, cases) } // GetDefaultingTestCases returns defaulting testcases for given scheme func GetDefaultingTestCases(t *testing.T, scheme *runtime.Scheme, codecs serializer.CodecFactory) []TestCase { cases := []TestCase{} for gvk := range scheme.AllKnownTypes() { if gvk.Version == runtime.APIVersionInternal { continue } beforeDir := fmt.Sprintf("testdata/%s/before", gvk.Kind) afterDir := fmt.Sprintf("testdata/%s/after", gvk.Kind) filename := fmt.Sprintf("%s.yaml", gvk.Version) codec, err := getCodecForGV(codecs, gvk.GroupVersion()) if err != nil { t.Fatal(err) } cases = append(cases, TestCase{ name: fmt.Sprintf("%s default_%s", gvk.Kind, gvk.Version), in: filepath.Join(beforeDir, filename), out: filepath.Join(afterDir, filename), codec: codec, }) } return cases } kubernetes-component-base-1b2882b/config/testing/helpers.go000066400000000000000000000061131476422213000240400ustar00rootroot00000000000000/* Copyright 2019 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 testing import ( "bytes" "fmt" "os" "path/filepath" "testing" "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" ) // TestCase defines a testcase for roundtrip and defaulting tests type TestCase struct { name, in, out string codec runtime.Codec } // RunTestsOnYAMLData decodes the yaml file from specified path, encodes the object and matches // with expected yaml in specified path func RunTestsOnYAMLData(t *testing.T, tests []TestCase) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { roundTrip(t, tc) }) } } func decodeYAML(t *testing.T, path string, codec runtime.Codec) runtime.Object { content, err := os.ReadFile(path) if err != nil { t.Fatal(err) } // decode to internal type object, err := runtime.Decode(codec, content) if err != nil { t.Fatal(err) } return object } func getCodecForGV(codecs serializer.CodecFactory, gv schema.GroupVersion) (runtime.Codec, error) { mediaType := runtime.ContentTypeYAML serializerInfo, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType) if !ok { return nil, fmt.Errorf("unable to locate encoder -- %q is not a supported media type", mediaType) } codec := codecs.CodecForVersions(serializerInfo.Serializer, codecs.UniversalDeserializer(), gv, nil) return codec, nil } func matchOutputFile(t *testing.T, actual []byte, expectedFilePath string) { expected, err := os.ReadFile(expectedFilePath) if err != nil && !os.IsNotExist(err) { t.Fatalf("couldn't read test data: %v", err) } needsUpdate := false const updateEnvVar = "UPDATE_COMPONENTCONFIG_FIXTURE_DATA" if os.IsNotExist(err) { needsUpdate = true if os.Getenv(updateEnvVar) != "true" { t.Error("couldn't find test data") } } else { if !bytes.Equal(expected, actual) { t.Errorf("Output does not match expected, diff (- want, + got):\n%s\n", cmp.Diff(string(expected), string(actual))) needsUpdate = true } } if needsUpdate { if os.Getenv(updateEnvVar) == "true" { if err := os.MkdirAll(filepath.Dir(expectedFilePath), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(expectedFilePath, actual, 0644); err != nil { t.Fatal(err) } t.Error("wrote expected test data... verify, commit, and rerun tests") } else { t.Errorf("if the diff is expected because of a new type or a new field, "+ "re-run with %s=true to update the compatibility data or generate missing files", updateEnvVar) } } } kubernetes-component-base-1b2882b/config/testing/roundtrip.go000066400000000000000000000066321476422213000244320ustar00rootroot00000000000000/* Copyright 2019 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 testing import ( "fmt" "os" "path/filepath" "testing" "github.com/google/go-cmp/cmp" apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" ) // RoundTripTest runs roundtrip tests for given scheme func RoundTripTest(t *testing.T, scheme *runtime.Scheme, codecs serializer.CodecFactory) { tc := GetRoundtripTestCases(t, scheme, codecs) RunTestsOnYAMLData(t, tc) } // GetRoundtripTestCases returns the testcases for roundtrip testing for given scheme func GetRoundtripTestCases(t *testing.T, scheme *runtime.Scheme, codecs serializer.CodecFactory) []TestCase { cases := []TestCase{} versionsForKind := map[schema.GroupKind][]string{} for gvk := range scheme.AllKnownTypes() { if gvk.Version != runtime.APIVersionInternal { versionsForKind[gvk.GroupKind()] = append(versionsForKind[gvk.GroupKind()], gvk.Version) } } for gk, versions := range versionsForKind { testdir := filepath.Join("testdata", gk.Kind, "roundtrip") dirs, err := os.ReadDir(testdir) if err != nil { t.Fatalf("failed to read testdir %s: %v", testdir, err) } for _, dir := range dirs { for _, vin := range versions { for _, vout := range versions { marshalGVK := gk.WithVersion(vout) codec, err := getCodecForGV(codecs, marshalGVK.GroupVersion()) if err != nil { t.Fatalf("failed to get codec for %v: %v", marshalGVK.GroupVersion().String(), err) } testname := dir.Name() cases = append(cases, TestCase{ name: fmt.Sprintf("%s_%sTo%s_%s", gk.Kind, vin, vout, testname), in: filepath.Join(testdir, testname, vin+".yaml"), out: filepath.Join(testdir, testname, vout+".yaml"), codec: codec, }) } } } } return cases } func roundTrip(t *testing.T, tc TestCase) { object := decodeYAML(t, tc.in, tc.codec) // original object of internal type original := object // encode (serialize) the object using the provided codec data, err := runtime.Encode(tc.codec, object) if err != nil { t.Fatalf("failed to encode object: %v", err) } // ensure that the encoding should not alter the object if !apiequality.Semantic.DeepEqual(original, object) { t.Fatalf("encode altered the object, diff (- want, + got): \n%v", cmp.Diff(original, object)) } // decode (deserialize) the encoded data back into an object obj2, err := runtime.Decode(tc.codec, data) if err != nil { t.Fatalf("failed to decode: %v", err) } // ensure that the object produced from decoding the encoded data is equal // to the original object if !apiequality.Semantic.DeepEqual(original, obj2) { t.Fatalf("object was not the same after roundtrip, diff (- want, + got):\n%v", cmp.Diff(object, obj2)) } // match with the input file, checks if they're the same after roundtrip matchOutputFile(t, data, tc.out) } kubernetes-component-base-1b2882b/config/types.go000066400000000000000000000065521476422213000220740ustar00rootroot00000000000000/* Copyright 2018 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 config import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // ClientConnectionConfiguration contains details for constructing a client. type ClientConnectionConfiguration struct { // kubeconfig is the path to a KubeConfig file. Kubeconfig string // acceptContentTypes defines the Accept header sent by clients when connecting to a server, overriding the // default value of 'application/json'. This field will control all connections to the server used by a particular // client. AcceptContentTypes string // contentType is the content type used when sending data to the server from this client. ContentType string // qps controls the number of queries per second allowed for this connection. QPS float32 // burst allows extra queries to accumulate when a client is exceeding its rate. Burst int32 } // LeaderElectionConfiguration defines the configuration of leader election // clients for components that can run with leader election enabled. type LeaderElectionConfiguration struct { // leaderElect enables a leader election client to gain leadership // before executing the main loop. Enable this when running replicated // components for high availability. LeaderElect bool // leaseDuration is the duration that non-leader candidates will wait // after observing a leadership renewal until attempting to acquire // leadership of a led but unrenewed leader slot. This is effectively the // maximum duration that a leader can be stopped before it is replaced // by another candidate. This is only applicable if leader election is // enabled. LeaseDuration metav1.Duration // renewDeadline is the interval between attempts by the acting master to // renew a leadership slot before it stops leading. This must be less // than or equal to the lease duration. This is only applicable if leader // election is enabled. RenewDeadline metav1.Duration // retryPeriod is the duration the clients should wait between attempting // acquisition and renewal of a leadership. This is only applicable if // leader election is enabled. RetryPeriod metav1.Duration // resourceLock indicates the resource object type that will be used to lock // during leader election cycles. ResourceLock string // resourceName indicates the name of resource object that will be used to lock // during leader election cycles. ResourceName string // resourceNamespace indicates the namespace of resource object that will be used to lock // during leader election cycles. ResourceNamespace string } // DebuggingConfiguration holds configuration for Debugging related features. type DebuggingConfiguration struct { // enableProfiling enables profiling via web interface host:port/debug/pprof/ EnableProfiling bool // enableContentionProfiling enables block profiling, if // enableProfiling is true. EnableContentionProfiling bool } kubernetes-component-base-1b2882b/config/v1alpha1/000077500000000000000000000000001476422213000220065ustar00rootroot00000000000000kubernetes-component-base-1b2882b/config/v1alpha1/conversion.go000066400000000000000000000054631476422213000245320ustar00rootroot00000000000000/* Copyright 2018 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 v1alpha1 import ( "k8s.io/apimachinery/pkg/conversion" "k8s.io/component-base/config" ) // Important! The public back-and-forth conversion functions for the types in this generic // package with ComponentConfig types need to be manually exposed like this in order for // other packages that reference this package to be able to call these conversion functions // in an autogenerated manner. // TODO: Fix the bug in conversion-gen so it automatically discovers these Convert_* functions // in autogenerated code as well. func Convert_v1alpha1_ClientConnectionConfiguration_To_config_ClientConnectionConfiguration(in *ClientConnectionConfiguration, out *config.ClientConnectionConfiguration, s conversion.Scope) error { return autoConvert_v1alpha1_ClientConnectionConfiguration_To_config_ClientConnectionConfiguration(in, out, s) } func Convert_config_ClientConnectionConfiguration_To_v1alpha1_ClientConnectionConfiguration(in *config.ClientConnectionConfiguration, out *ClientConnectionConfiguration, s conversion.Scope) error { return autoConvert_config_ClientConnectionConfiguration_To_v1alpha1_ClientConnectionConfiguration(in, out, s) } func Convert_v1alpha1_DebuggingConfiguration_To_config_DebuggingConfiguration(in *DebuggingConfiguration, out *config.DebuggingConfiguration, s conversion.Scope) error { return autoConvert_v1alpha1_DebuggingConfiguration_To_config_DebuggingConfiguration(in, out, s) } func Convert_config_DebuggingConfiguration_To_v1alpha1_DebuggingConfiguration(in *config.DebuggingConfiguration, out *DebuggingConfiguration, s conversion.Scope) error { return autoConvert_config_DebuggingConfiguration_To_v1alpha1_DebuggingConfiguration(in, out, s) } func Convert_v1alpha1_LeaderElectionConfiguration_To_config_LeaderElectionConfiguration(in *LeaderElectionConfiguration, out *config.LeaderElectionConfiguration, s conversion.Scope) error { return autoConvert_v1alpha1_LeaderElectionConfiguration_To_config_LeaderElectionConfiguration(in, out, s) } func Convert_config_LeaderElectionConfiguration_To_v1alpha1_LeaderElectionConfiguration(in *config.LeaderElectionConfiguration, out *LeaderElectionConfiguration, s conversion.Scope) error { return autoConvert_config_LeaderElectionConfiguration_To_v1alpha1_LeaderElectionConfiguration(in, out, s) } kubernetes-component-base-1b2882b/config/v1alpha1/defaults.go000066400000000000000000000103721476422213000241470ustar00rootroot00000000000000/* Copyright 2018 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 v1alpha1 import ( "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilpointer "k8s.io/utils/pointer" ) // RecommendedDefaultLeaderElectionConfiguration defaults a pointer to a // LeaderElectionConfiguration struct. This will set the recommended default // values, but they may be subject to change between API versions. This function // is intentionally not registered in the scheme as a "normal" `SetDefaults_Foo` // function to allow consumers of this type to set whatever defaults for their // embedded configs. Forcing consumers to use these defaults would be problematic // as defaulting in the scheme is done as part of the conversion, and there would // be no easy way to opt-out. Instead, if you want to use this defaulting method // run it in your wrapper struct of this type in its `SetDefaults_` method. func RecommendedDefaultLeaderElectionConfiguration(obj *LeaderElectionConfiguration) { zero := metav1.Duration{} if obj.LeaseDuration == zero { obj.LeaseDuration = metav1.Duration{Duration: 15 * time.Second} } if obj.RenewDeadline == zero { obj.RenewDeadline = metav1.Duration{Duration: 10 * time.Second} } if obj.RetryPeriod == zero { obj.RetryPeriod = metav1.Duration{Duration: 2 * time.Second} } if obj.ResourceLock == "" { // TODO(#80289): Figure out how to migrate to LeaseLock at this point. // This will most probably require going through EndpointsLease first. obj.ResourceLock = EndpointsResourceLock } if obj.LeaderElect == nil { obj.LeaderElect = utilpointer.BoolPtr(true) } } // RecommendedDefaultClientConnectionConfiguration defaults a pointer to a // ClientConnectionConfiguration struct. This will set the recommended default // values, but they may be subject to change between API versions. This function // is intentionally not registered in the scheme as a "normal" `SetDefaults_Foo` // function to allow consumers of this type to set whatever defaults for their // embedded configs. Forcing consumers to use these defaults would be problematic // as defaulting in the scheme is done as part of the conversion, and there would // be no easy way to opt-out. Instead, if you want to use this defaulting method // run it in your wrapper struct of this type in its `SetDefaults_` method. func RecommendedDefaultClientConnectionConfiguration(obj *ClientConnectionConfiguration) { if len(obj.ContentType) == 0 { obj.ContentType = "application/vnd.kubernetes.protobuf" } if obj.QPS == 0.0 { obj.QPS = 50.0 } if obj.Burst == 0 { obj.Burst = 100 } } // RecommendedDebuggingConfiguration defaults profiling and debugging configuration. // This will set the recommended default // values, but they may be subject to change between API versions. This function // is intentionally not registered in the scheme as a "normal" `SetDefaults_Foo` // function to allow consumers of this type to set whatever defaults for their // embedded configs. Forcing consumers to use these defaults would be problematic // as defaulting in the scheme is done as part of the conversion, and there would // be no easy way to opt-out. Instead, if you want to use this defaulting method // run it in your wrapper struct of this type in its `SetDefaults_` method. func RecommendedDebuggingConfiguration(obj *DebuggingConfiguration) { if obj.EnableProfiling == nil { obj.EnableProfiling = utilpointer.BoolPtr(true) // profile debugging is cheap to have exposed and standard on kube binaries } } // NewRecommendedDebuggingConfiguration returns the current recommended DebuggingConfiguration. // This may change between releases as recommendations shift. func NewRecommendedDebuggingConfiguration() *DebuggingConfiguration { ret := &DebuggingConfiguration{} RecommendedDebuggingConfiguration(ret) return ret } kubernetes-component-base-1b2882b/config/v1alpha1/doc.go000066400000000000000000000013201476422213000230760ustar00rootroot00000000000000/* Copyright 2018 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. */ // +k8s:deepcopy-gen=package // +k8s:conversion-gen=k8s.io/component-base/config package v1alpha1 // import "k8s.io/component-base/config/v1alpha1" kubernetes-component-base-1b2882b/config/v1alpha1/register.go000066400000000000000000000021311476422213000241560ustar00rootroot00000000000000/* Copyright 2018 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 v1alpha1 import ( "k8s.io/apimachinery/pkg/runtime" ) var ( // SchemeBuilder is the scheme builder with scheme init functions to run for this API package SchemeBuilder runtime.SchemeBuilder // localSchemeBuilder extends the SchemeBuilder instance with the external types. In this package, // defaulting and conversion init funcs are registered as well. localSchemeBuilder = &SchemeBuilder // AddToScheme is a global function that registers this API group & version to a scheme AddToScheme = localSchemeBuilder.AddToScheme ) kubernetes-component-base-1b2882b/config/v1alpha1/types.go000066400000000000000000000073451476422213000235120ustar00rootroot00000000000000/* Copyright 2018 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 v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const EndpointsResourceLock = "endpoints" // LeaderElectionConfiguration defines the configuration of leader election // clients for components that can run with leader election enabled. type LeaderElectionConfiguration struct { // leaderElect enables a leader election client to gain leadership // before executing the main loop. Enable this when running replicated // components for high availability. LeaderElect *bool `json:"leaderElect"` // leaseDuration is the duration that non-leader candidates will wait // after observing a leadership renewal until attempting to acquire // leadership of a led but unrenewed leader slot. This is effectively the // maximum duration that a leader can be stopped before it is replaced // by another candidate. This is only applicable if leader election is // enabled. LeaseDuration metav1.Duration `json:"leaseDuration"` // renewDeadline is the interval between attempts by the acting master to // renew a leadership slot before it stops leading. This must be less // than or equal to the lease duration. This is only applicable if leader // election is enabled. RenewDeadline metav1.Duration `json:"renewDeadline"` // retryPeriod is the duration the clients should wait between attempting // acquisition and renewal of a leadership. This is only applicable if // leader election is enabled. RetryPeriod metav1.Duration `json:"retryPeriod"` // resourceLock indicates the resource object type that will be used to lock // during leader election cycles. ResourceLock string `json:"resourceLock"` // resourceName indicates the name of resource object that will be used to lock // during leader election cycles. ResourceName string `json:"resourceName"` // resourceName indicates the namespace of resource object that will be used to lock // during leader election cycles. ResourceNamespace string `json:"resourceNamespace"` } // DebuggingConfiguration holds configuration for Debugging related features. type DebuggingConfiguration struct { // enableProfiling enables profiling via web interface host:port/debug/pprof/ EnableProfiling *bool `json:"enableProfiling,omitempty"` // enableContentionProfiling enables block profiling, if // enableProfiling is true. EnableContentionProfiling *bool `json:"enableContentionProfiling,omitempty"` } // ClientConnectionConfiguration contains details for constructing a client. type ClientConnectionConfiguration struct { // kubeconfig is the path to a KubeConfig file. Kubeconfig string `json:"kubeconfig"` // acceptContentTypes defines the Accept header sent by clients when connecting to a server, overriding the // default value of 'application/json'. This field will control all connections to the server used by a particular // client. AcceptContentTypes string `json:"acceptContentTypes"` // contentType is the content type used when sending data to the server from this client. ContentType string `json:"contentType"` // qps controls the number of queries per second allowed for this connection. QPS float32 `json:"qps"` // burst allows extra queries to accumulate when a client is exceeding its rate. Burst int32 `json:"burst"` } kubernetes-component-base-1b2882b/config/v1alpha1/zz_generated.conversion.go000066400000000000000000000137031476422213000272060ustar00rootroot00000000000000//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 v1alpha1 import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" config "k8s.io/component-base/config" ) 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.AddConversionFunc((*config.ClientConnectionConfiguration)(nil), (*ClientConnectionConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_config_ClientConnectionConfiguration_To_v1alpha1_ClientConnectionConfiguration(a.(*config.ClientConnectionConfiguration), b.(*ClientConnectionConfiguration), scope) }); err != nil { return err } if err := s.AddConversionFunc((*config.DebuggingConfiguration)(nil), (*DebuggingConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_config_DebuggingConfiguration_To_v1alpha1_DebuggingConfiguration(a.(*config.DebuggingConfiguration), b.(*DebuggingConfiguration), scope) }); err != nil { return err } if err := s.AddConversionFunc((*config.LeaderElectionConfiguration)(nil), (*LeaderElectionConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_config_LeaderElectionConfiguration_To_v1alpha1_LeaderElectionConfiguration(a.(*config.LeaderElectionConfiguration), b.(*LeaderElectionConfiguration), scope) }); err != nil { return err } if err := s.AddConversionFunc((*ClientConnectionConfiguration)(nil), (*config.ClientConnectionConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_ClientConnectionConfiguration_To_config_ClientConnectionConfiguration(a.(*ClientConnectionConfiguration), b.(*config.ClientConnectionConfiguration), scope) }); err != nil { return err } if err := s.AddConversionFunc((*DebuggingConfiguration)(nil), (*config.DebuggingConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_DebuggingConfiguration_To_config_DebuggingConfiguration(a.(*DebuggingConfiguration), b.(*config.DebuggingConfiguration), scope) }); err != nil { return err } if err := s.AddConversionFunc((*LeaderElectionConfiguration)(nil), (*config.LeaderElectionConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_LeaderElectionConfiguration_To_config_LeaderElectionConfiguration(a.(*LeaderElectionConfiguration), b.(*config.LeaderElectionConfiguration), scope) }); err != nil { return err } return nil } func autoConvert_v1alpha1_ClientConnectionConfiguration_To_config_ClientConnectionConfiguration(in *ClientConnectionConfiguration, out *config.ClientConnectionConfiguration, s conversion.Scope) error { out.Kubeconfig = in.Kubeconfig out.AcceptContentTypes = in.AcceptContentTypes out.ContentType = in.ContentType out.QPS = in.QPS out.Burst = in.Burst return nil } func autoConvert_config_ClientConnectionConfiguration_To_v1alpha1_ClientConnectionConfiguration(in *config.ClientConnectionConfiguration, out *ClientConnectionConfiguration, s conversion.Scope) error { out.Kubeconfig = in.Kubeconfig out.AcceptContentTypes = in.AcceptContentTypes out.ContentType = in.ContentType out.QPS = in.QPS out.Burst = in.Burst return nil } func autoConvert_v1alpha1_DebuggingConfiguration_To_config_DebuggingConfiguration(in *DebuggingConfiguration, out *config.DebuggingConfiguration, s conversion.Scope) error { if err := v1.Convert_Pointer_bool_To_bool(&in.EnableProfiling, &out.EnableProfiling, s); err != nil { return err } if err := v1.Convert_Pointer_bool_To_bool(&in.EnableContentionProfiling, &out.EnableContentionProfiling, s); err != nil { return err } return nil } func autoConvert_config_DebuggingConfiguration_To_v1alpha1_DebuggingConfiguration(in *config.DebuggingConfiguration, out *DebuggingConfiguration, s conversion.Scope) error { if err := v1.Convert_bool_To_Pointer_bool(&in.EnableProfiling, &out.EnableProfiling, s); err != nil { return err } if err := v1.Convert_bool_To_Pointer_bool(&in.EnableContentionProfiling, &out.EnableContentionProfiling, s); err != nil { return err } return nil } func autoConvert_v1alpha1_LeaderElectionConfiguration_To_config_LeaderElectionConfiguration(in *LeaderElectionConfiguration, out *config.LeaderElectionConfiguration, s conversion.Scope) error { if err := v1.Convert_Pointer_bool_To_bool(&in.LeaderElect, &out.LeaderElect, s); err != nil { return err } out.LeaseDuration = in.LeaseDuration out.RenewDeadline = in.RenewDeadline out.RetryPeriod = in.RetryPeriod out.ResourceLock = in.ResourceLock out.ResourceName = in.ResourceName out.ResourceNamespace = in.ResourceNamespace return nil } func autoConvert_config_LeaderElectionConfiguration_To_v1alpha1_LeaderElectionConfiguration(in *config.LeaderElectionConfiguration, out *LeaderElectionConfiguration, s conversion.Scope) error { if err := v1.Convert_bool_To_Pointer_bool(&in.LeaderElect, &out.LeaderElect, s); err != nil { return err } out.LeaseDuration = in.LeaseDuration out.RenewDeadline = in.RenewDeadline out.RetryPeriod = in.RetryPeriod out.ResourceLock = in.ResourceLock out.ResourceName = in.ResourceName out.ResourceNamespace = in.ResourceNamespace return nil } kubernetes-component-base-1b2882b/config/v1alpha1/zz_generated.deepcopy.go000066400000000000000000000053361476422213000266340ustar00rootroot00000000000000//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 v1alpha1 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClientConnectionConfiguration) DeepCopyInto(out *ClientConnectionConfiguration) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientConnectionConfiguration. func (in *ClientConnectionConfiguration) DeepCopy() *ClientConnectionConfiguration { if in == nil { return nil } out := new(ClientConnectionConfiguration) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DebuggingConfiguration) DeepCopyInto(out *DebuggingConfiguration) { *out = *in if in.EnableProfiling != nil { in, out := &in.EnableProfiling, &out.EnableProfiling *out = new(bool) **out = **in } if in.EnableContentionProfiling != nil { in, out := &in.EnableContentionProfiling, &out.EnableContentionProfiling *out = new(bool) **out = **in } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DebuggingConfiguration. func (in *DebuggingConfiguration) DeepCopy() *DebuggingConfiguration { if in == nil { return nil } out := new(DebuggingConfiguration) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LeaderElectionConfiguration) DeepCopyInto(out *LeaderElectionConfiguration) { *out = *in if in.LeaderElect != nil { in, out := &in.LeaderElect, &out.LeaderElect *out = new(bool) **out = **in } out.LeaseDuration = in.LeaseDuration out.RenewDeadline = in.RenewDeadline out.RetryPeriod = in.RetryPeriod return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LeaderElectionConfiguration. func (in *LeaderElectionConfiguration) DeepCopy() *LeaderElectionConfiguration { if in == nil { return nil } out := new(LeaderElectionConfiguration) in.DeepCopyInto(out) return out } kubernetes-component-base-1b2882b/config/validation/000077500000000000000000000000001476422213000225235ustar00rootroot00000000000000kubernetes-component-base-1b2882b/config/validation/validation.go000066400000000000000000000050051476422213000252040ustar00rootroot00000000000000/* Copyright 2018 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 validation import ( "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/component-base/config" ) // ValidateClientConnectionConfiguration ensures validation of the ClientConnectionConfiguration struct func ValidateClientConnectionConfiguration(cc *config.ClientConnectionConfiguration, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if cc.Burst < 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("burst"), cc.Burst, "must be non-negative")) } return allErrs } // ValidateLeaderElectionConfiguration ensures validation of the LeaderElectionConfiguration struct func ValidateLeaderElectionConfiguration(cc *config.LeaderElectionConfiguration, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if !cc.LeaderElect { return allErrs } if cc.LeaseDuration.Duration <= 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("leaseDuration"), cc.LeaseDuration, "must be greater than zero")) } if cc.RenewDeadline.Duration <= 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("renewDeadline"), cc.RenewDeadline, "must be greater than zero")) } if cc.RetryPeriod.Duration <= 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("retryPeriod"), cc.RetryPeriod, "must be greater than zero")) } if cc.LeaseDuration.Duration <= cc.RenewDeadline.Duration { allErrs = append(allErrs, field.Invalid(fldPath.Child("leaseDuration"), cc.RenewDeadline, "LeaseDuration must be greater than RenewDeadline")) } if len(cc.ResourceLock) == 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceLock"), cc.ResourceLock, "resourceLock is required")) } if len(cc.ResourceNamespace) == 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceNamespace"), cc.ResourceNamespace, "resourceNamespace is required")) } if len(cc.ResourceName) == 0 { allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceName"), cc.ResourceName, "resourceName is required")) } return allErrs } kubernetes-component-base-1b2882b/config/validation/validation_test.go000066400000000000000000000132151476422213000262450ustar00rootroot00000000000000/* Copyright 2018 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 validation import ( "testing" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/component-base/config" ) func TestValidateClientConnectionConfiguration(t *testing.T) { validConfig := &config.ClientConnectionConfiguration{ AcceptContentTypes: "application/json", ContentType: "application/json", QPS: 10, Burst: 10, } qpsLessThanZero := validConfig.DeepCopy() qpsLessThanZero.QPS = -1 burstLessThanZero := validConfig.DeepCopy() burstLessThanZero.Burst = -1 scenarios := map[string]struct { expectedToFail bool config *config.ClientConnectionConfiguration }{ "good": { expectedToFail: false, config: validConfig, }, "good-qps-less-than-zero": { expectedToFail: false, config: qpsLessThanZero, }, "bad-burst-less-then-zero": { expectedToFail: true, config: burstLessThanZero, }, } for name, scenario := range scenarios { errs := ValidateClientConnectionConfiguration(scenario.config, field.NewPath("clientConnectionConfiguration")) if len(errs) == 0 && scenario.expectedToFail { t.Errorf("Unexpected success for scenario: %s", name) } if len(errs) > 0 && !scenario.expectedToFail { t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) } } } func TestValidateLeaderElectionConfiguration(t *testing.T) { validConfig := &config.LeaderElectionConfiguration{ ResourceLock: "configmap", LeaderElect: true, LeaseDuration: metav1.Duration{Duration: 30 * time.Second}, RenewDeadline: metav1.Duration{Duration: 15 * time.Second}, RetryPeriod: metav1.Duration{Duration: 5 * time.Second}, ResourceNamespace: "namespace", ResourceName: "name", } renewDeadlineEqualToLeaseDuration := validConfig.DeepCopy() renewDeadlineEqualToLeaseDuration.RenewDeadline = metav1.Duration{Duration: 30 * time.Second} renewDeadlineExceedsLeaseDuration := validConfig.DeepCopy() renewDeadlineExceedsLeaseDuration.RenewDeadline = metav1.Duration{Duration: 45 * time.Second} renewDeadlineZero := validConfig.DeepCopy() renewDeadlineZero.RenewDeadline = metav1.Duration{Duration: 0 * time.Second} leaseDurationZero := validConfig.DeepCopy() leaseDurationZero.LeaseDuration = metav1.Duration{Duration: 0 * time.Second} negativeValForRetryPeriod := validConfig.DeepCopy() negativeValForRetryPeriod.RetryPeriod = metav1.Duration{Duration: -45 * time.Second} negativeValForLeaseDuration := validConfig.DeepCopy() negativeValForLeaseDuration.LeaseDuration = metav1.Duration{Duration: -45 * time.Second} negativeValForRenewDeadline := validConfig.DeepCopy() negativeValForRenewDeadline.RenewDeadline = metav1.Duration{Duration: -45 * time.Second} LeaderElectButLeaderElectNotEnabled := validConfig.DeepCopy() LeaderElectButLeaderElectNotEnabled.LeaderElect = false LeaderElectButLeaderElectNotEnabled.LeaseDuration = metav1.Duration{Duration: -45 * time.Second} resourceLockNotDefined := validConfig.DeepCopy() resourceLockNotDefined.ResourceLock = "" resourceNameNotDefined := validConfig.DeepCopy() resourceNameNotDefined.ResourceName = "" resourceNamespaceNotDefined := validConfig.DeepCopy() resourceNamespaceNotDefined.ResourceNamespace = "" scenarios := map[string]struct { expectedToFail bool config *config.LeaderElectionConfiguration }{ "good": { expectedToFail: false, config: validConfig, }, "good-dont-check-leader-config-if-not-enabled": { expectedToFail: false, config: LeaderElectButLeaderElectNotEnabled, }, "bad-renew-deadline-equal-to-lease-duration": { expectedToFail: true, config: renewDeadlineEqualToLeaseDuration, }, "bad-renew-deadline-exceeds-lease-duration": { expectedToFail: true, config: renewDeadlineExceedsLeaseDuration, }, "bad-negative-value-for-retry-period": { expectedToFail: true, config: negativeValForRetryPeriod, }, "bad-negative-value-for-lease-duration": { expectedToFail: true, config: negativeValForLeaseDuration, }, "bad-negative-value-for-renew-deadline": { expectedToFail: true, config: negativeValForRenewDeadline, }, "bad-renew-deadline-zero": { expectedToFail: true, config: renewDeadlineZero, }, "bad-lease-duration-zero": { expectedToFail: true, config: leaseDurationZero, }, "bad-resource-lock-not-defined": { expectedToFail: true, config: resourceLockNotDefined, }, "bad-resource-name-not-defined": { expectedToFail: true, config: resourceNameNotDefined, }, "bad-resource-namespace-not-defined": { expectedToFail: true, config: resourceNamespaceNotDefined, }, } for name, scenario := range scenarios { errs := ValidateLeaderElectionConfiguration(scenario.config, field.NewPath("leaderElectionConfiguration")) if len(errs) == 0 && scenario.expectedToFail { t.Errorf("Unexpected success for scenario: %s", name) } if len(errs) > 0 && !scenario.expectedToFail { t.Errorf("Unexpected failure for scenario: %s - %+v", name, errs) } } } kubernetes-component-base-1b2882b/config/zz_generated.deepcopy.go000066400000000000000000000045261476422213000252170ustar00rootroot00000000000000//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 config // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClientConnectionConfiguration) DeepCopyInto(out *ClientConnectionConfiguration) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientConnectionConfiguration. func (in *ClientConnectionConfiguration) DeepCopy() *ClientConnectionConfiguration { if in == nil { return nil } out := new(ClientConnectionConfiguration) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DebuggingConfiguration) DeepCopyInto(out *DebuggingConfiguration) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DebuggingConfiguration. func (in *DebuggingConfiguration) DeepCopy() *DebuggingConfiguration { if in == nil { return nil } out := new(DebuggingConfiguration) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LeaderElectionConfiguration) DeepCopyInto(out *LeaderElectionConfiguration) { *out = *in out.LeaseDuration = in.LeaseDuration out.RenewDeadline = in.RenewDeadline out.RetryPeriod = in.RetryPeriod return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LeaderElectionConfiguration. func (in *LeaderElectionConfiguration) DeepCopy() *LeaderElectionConfiguration { if in == nil { return nil } out := new(LeaderElectionConfiguration) in.DeepCopyInto(out) return out } kubernetes-component-base-1b2882b/configz/000077500000000000000000000000001476422213000205635ustar00rootroot00000000000000kubernetes-component-base-1b2882b/configz/OWNERS000066400000000000000000000006471476422213000215320ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners # For lack of a better idea, assigned configz to api-approvers because # configz is an API that has been around for a long time, even if we don't # guarantee its stability. # Disable inheritance as this is an api owners file options: no_parent_owners: true approvers: - api-approvers reviewers: - api-reviewers labels: - kind/api-change - sig/cluster-lifecycle kubernetes-component-base-1b2882b/configz/configz.go000066400000000000000000000062461476422213000225610ustar00rootroot00000000000000/* Copyright 2015 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 configz serves ComponentConfig objects from running components. // // Each component that wants to serve its ComponentConfig creates a Config // object, and the program should call InstallHandler once. e.g., // // func main() { // boatConfig := getBoatConfig() // planeConfig := getPlaneConfig() // // bcz, err := configz.New("boat") // if err != nil { // panic(err) // } // bcz.Set(boatConfig) // // pcz, err := configz.New("plane") // if err != nil { // panic(err) // } // pcz.Set(planeConfig) // // configz.InstallHandler(http.DefaultServeMux) // http.ListenAndServe(":8080", http.DefaultServeMux) // } package configz import ( "encoding/json" "fmt" "net/http" "sync" ) const DefaultConfigzPath = "/configz" var ( configsGuard sync.RWMutex configs = map[string]*Config{} ) // Config is a handle to a ComponentConfig object. Don't create these directly; // use New() instead. type Config struct { val interface{} } // InstallHandler adds an HTTP handler on the given mux for the "/configz" // endpoint which serves all registered ComponentConfigs in JSON format. func InstallHandler(m mux) { m.Handle(DefaultConfigzPath, http.HandlerFunc(handle)) } type mux interface { Handle(string, http.Handler) } // New creates a Config object with the given name. Each Config is registered // with this package's "/configz" handler. func New(name string) (*Config, error) { configsGuard.Lock() defer configsGuard.Unlock() if _, found := configs[name]; found { return nil, fmt.Errorf("register config %q twice", name) } newConfig := Config{} configs[name] = &newConfig return &newConfig, nil } // Delete removes the named ComponentConfig from this package's "/configz" // handler. func Delete(name string) { configsGuard.Lock() defer configsGuard.Unlock() delete(configs, name) } // Set sets the ComponentConfig for this Config. func (v *Config) Set(val interface{}) { configsGuard.Lock() defer configsGuard.Unlock() v.val = val } // MarshalJSON marshals the ComponentConfig as JSON data. func (v *Config) MarshalJSON() ([]byte, error) { return json.Marshal(v.val) } func handle(w http.ResponseWriter, r *http.Request) { if err := write(w); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func write(w http.ResponseWriter) error { var b []byte var err error func() { configsGuard.RLock() defer configsGuard.RUnlock() b, err = json.Marshal(configs) }() if err != nil { return fmt.Errorf("error marshaling json: %v", err) } w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Content-Type-Options", "nosniff") _, err = w.Write(b) return err } kubernetes-component-base-1b2882b/configz/configz_test.go000066400000000000000000000033701476422213000236130ustar00rootroot00000000000000/* Copyright 2015 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 configz import ( "io" "net/http" "net/http/httptest" "testing" ) func TestConfigz(t *testing.T) { v, err := New("testing") if err != nil { t.Fatalf("err: %v", err) } v.Set("blah") s := httptest.NewServer(http.HandlerFunc(handle)) defer s.Close() resp, err := http.Get(s.URL + "/configz") if err != nil { t.Fatalf("err: %v", err) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("err: %v", err) } if string(body) != `{"testing":"blah"}` { t.Fatalf("unexpected output: %s", body) } v.Set("bing") resp, err = http.Get(s.URL + "/configz") if err != nil { t.Fatalf("err: %v", err) } body, err = io.ReadAll(resp.Body) if err != nil { t.Fatalf("err: %v", err) } if string(body) != `{"testing":"bing"}` { t.Fatalf("unexpected output: %s", body) } Delete("testing") resp, err = http.Get(s.URL + "/configz") if err != nil { t.Fatalf("err: %v", err) } body, err = io.ReadAll(resp.Body) if err != nil { t.Fatalf("err: %v", err) } if string(body) != `{}` { t.Fatalf("unexpected output: %s", body) } if resp.Header.Get("Content-Type") != "application/json" { t.Fatalf("unexpected Content-Type: %s", resp.Header.Get("Content-Type")) } } kubernetes-component-base-1b2882b/doc.go000066400000000000000000000011631476422213000202210ustar00rootroot00000000000000/* Copyright 2021 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 componentbase // import "k8s.io/component-base" kubernetes-component-base-1b2882b/featuregate/000077500000000000000000000000001476422213000214205ustar00rootroot00000000000000kubernetes-component-base-1b2882b/featuregate/OWNERS000066400000000000000000000005731476422213000223650ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners # Currently assigned to api-approvers since feature gates are the API # for enabling/disabling other APIs. # Disable inheritance as this is an api owners file options: no_parent_owners: true approvers: - api-approvers reviewers: - api-reviewers labels: - kind/api-change - sig/api-machinery - sig/cluster-lifecycle kubernetes-component-base-1b2882b/featuregate/feature_gate.go000066400000000000000000000646351476422213000244200ustar00rootroot00000000000000/* Copyright 2016 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 featuregate import ( "context" "fmt" "reflect" "sort" "strconv" "strings" "sync" "sync/atomic" "github.com/spf13/pflag" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/naming" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/version" featuremetrics "k8s.io/component-base/metrics/prometheus/feature" baseversion "k8s.io/component-base/version" "k8s.io/klog/v2" ) type Feature string const ( flagName = "feature-gates" // allAlphaGate is a global toggle for alpha features. Per-feature key // values override the default set by allAlphaGate. Examples: // AllAlpha=false,NewFeature=true will result in newFeature=true // AllAlpha=true,NewFeature=false will result in newFeature=false allAlphaGate Feature = "AllAlpha" // allBetaGate is a global toggle for beta features. Per-feature key // values override the default set by allBetaGate. Examples: // AllBeta=false,NewFeature=true will result in NewFeature=true // AllBeta=true,NewFeature=false will result in NewFeature=false allBetaGate Feature = "AllBeta" ) var ( // The generic features. defaultFeatures = map[Feature]VersionedSpecs{ allAlphaGate: {{Default: false, PreRelease: Alpha, Version: version.MajorMinor(0, 0)}}, allBetaGate: {{Default: false, PreRelease: Beta, Version: version.MajorMinor(0, 0)}}, } // Special handling for a few gates. specialFeatures = map[Feature]func(known map[Feature]VersionedSpecs, enabled map[Feature]bool, val bool, cVer *version.Version){ allAlphaGate: setUnsetAlphaGates, allBetaGate: setUnsetBetaGates, } ) type FeatureSpec struct { // Default is the default enablement state for the feature Default bool // LockToDefault indicates that the feature is locked to its default and cannot be changed LockToDefault bool // PreRelease indicates the current maturity level of the feature PreRelease prerelease // Version indicates the earliest version from which this FeatureSpec is valid. // If multiple FeatureSpecs exist for a Feature, the one with the highest version that is less // than or equal to the effective version of the component is used. Version *version.Version } type VersionedSpecs []FeatureSpec func (g VersionedSpecs) Len() int { return len(g) } func (g VersionedSpecs) Less(i, j int) bool { return g[i].Version.LessThan(g[j].Version) } func (g VersionedSpecs) Swap(i, j int) { g[i], g[j] = g[j], g[i] } type PromotionVersionMapping map[prerelease]string type prerelease string const ( PreAlpha = prerelease("PRE-ALPHA") // Values for PreRelease. Alpha = prerelease("ALPHA") Beta = prerelease("BETA") GA = prerelease("") // Deprecated Deprecated = prerelease("DEPRECATED") ) // FeatureGate indicates whether a given feature is enabled or not type FeatureGate interface { // Enabled returns true if the key is enabled. Enabled(key Feature) bool // KnownFeatures returns a slice of strings describing the FeatureGate's known features. KnownFeatures() []string // DeepCopy returns a deep copy of the FeatureGate object, such that gates can be // set on the copy without mutating the original. This is useful for validating // config against potential feature gate changes before committing those changes. DeepCopy() MutableVersionedFeatureGate // Validate checks if the flag gates are valid at the emulated version. Validate() []error } // MutableFeatureGate parses and stores flag gates for known features from // a string like feature1=true,feature2=false,... type MutableFeatureGate interface { FeatureGate // AddFlag adds a flag for setting global feature gates to the specified FlagSet. AddFlag(fs *pflag.FlagSet) // Close sets closed to true, and prevents subsequent calls to Add Close() // Set parses and stores flag gates for known features // from a string like feature1=true,feature2=false,... Set(value string) error // SetFromMap stores flag gates for known features from a map[string]bool or returns an error SetFromMap(m map[string]bool) error // Add adds features to the featureGate. Add(features map[Feature]FeatureSpec) error // GetAll returns a copy of the map of known feature names to feature specs. GetAll() map[Feature]FeatureSpec // AddMetrics adds feature enablement metrics AddMetrics() // OverrideDefault sets a local override for the registered default value of a named // feature. If the feature has not been previously registered (e.g. by a call to Add), has a // locked default, or if the gate has already registered itself with a FlagSet, a non-nil // error is returned. // // When two or more components consume a common feature, one component can override its // default at runtime in order to adopt new defaults before or after the other // components. For example, a new feature can be evaluated with a limited blast radius by // overriding its default to true for a limited number of components without simultaneously // changing its default for all consuming components. OverrideDefault(name Feature, override bool) error } // MutableVersionedFeatureGate parses and stores flag gates for known features from // a string like feature1=true,feature2=false,... // MutableVersionedFeatureGate sets options based on the emulated version of the featured gate. type MutableVersionedFeatureGate interface { MutableFeatureGate // EmulationVersion returns the version the feature gate is set to emulate. // If set, the feature gate would enable/disable features based on // feature availability and pre-release at the emulated version instead of the binary version. EmulationVersion() *version.Version // SetEmulationVersion overrides the emulationVersion of the feature gate. // Otherwise, the emulationVersion will be the same as the binary version. // If set, the feature defaults and availability will be as if the binary is at the emulated version. SetEmulationVersion(emulationVersion *version.Version) error // GetAll returns a copy of the map of known feature names to versioned feature specs. GetAllVersioned() map[Feature]VersionedSpecs // AddVersioned adds versioned feature specs to the featureGate. AddVersioned(features map[Feature]VersionedSpecs) error // OverrideDefaultAtVersion sets a local override for the registered default value of a named // feature for the prerelease lifecycle the given version is at. // If the feature has not been previously registered (e.g. by a call to Add), // has a locked default, or if the gate has already registered itself with a FlagSet, a non-nil // error is returned. // // When two or more components consume a common feature, one component can override its // default at runtime in order to adopt new defaults before or after the other // components. For example, a new feature can be evaluated with a limited blast radius by // overriding its default to true for a limited number of components without simultaneously // changing its default for all consuming components. OverrideDefaultAtVersion(name Feature, override bool, ver *version.Version) error // ExplicitlySet returns true if the feature value is explicitly set instead of // being derived from the default values or special features. ExplicitlySet(name Feature) bool // ResetFeatureValueToDefault resets the value of the feature back to the default value. ResetFeatureValueToDefault(name Feature) error // DeepCopyAndReset copies all the registered features of the FeatureGate object, with all the known features and overrides, // and resets all the enabled status of the new feature gate. // This is useful for creating a new instance of feature gate without inheriting all the enabled configurations of the base feature gate. DeepCopyAndReset() MutableVersionedFeatureGate } // featureGate implements FeatureGate as well as pflag.Value for flag parsing. type featureGate struct { featureGateName string special map[Feature]func(map[Feature]VersionedSpecs, map[Feature]bool, bool, *version.Version) // lock guards writes to all below fields. lock sync.Mutex // known holds a map[Feature]FeatureSpec known atomic.Value // enabled holds a map[Feature]bool enabled atomic.Value // enabledRaw holds a raw map[string]bool of the parsed flag. // It keeps the original values of "special" features like "all alpha gates", // while enabled keeps the values of all resolved features. enabledRaw atomic.Value // closed is set to true when AddFlag is called, and prevents subsequent calls to Add closed bool // queriedFeatures stores all the features that have been queried through the Enabled interface. // It is reset when SetEmulationVersion is called. queriedFeatures atomic.Value emulationVersion atomic.Pointer[version.Version] } func setUnsetAlphaGates(known map[Feature]VersionedSpecs, enabled map[Feature]bool, val bool, cVer *version.Version) { for k, v := range known { if k == "AllAlpha" || k == "AllBeta" { continue } featureSpec := featureSpecAtEmulationVersion(v, cVer) if featureSpec.PreRelease == Alpha { if _, found := enabled[k]; !found { enabled[k] = val } } } } func setUnsetBetaGates(known map[Feature]VersionedSpecs, enabled map[Feature]bool, val bool, cVer *version.Version) { for k, v := range known { if k == "AllAlpha" || k == "AllBeta" { continue } featureSpec := featureSpecAtEmulationVersion(v, cVer) if featureSpec.PreRelease == Beta { if _, found := enabled[k]; !found { enabled[k] = val } } } } // Set, String, and Type implement pflag.Value var _ pflag.Value = &featureGate{} // internalPackages are packages that ignored when creating a name for featureGates. These packages are in the common // call chains, so they'd be unhelpful as names. var internalPackages = []string{"k8s.io/component-base/featuregate/feature_gate.go"} // NewVersionedFeatureGate creates a feature gate with the emulation version set to the provided version. // SetEmulationVersion can be called after to change emulation version to a desired value. func NewVersionedFeatureGate(emulationVersion *version.Version) *featureGate { known := map[Feature]VersionedSpecs{} for k, v := range defaultFeatures { known[k] = v } f := &featureGate{ featureGateName: naming.GetNameFromCallsite(internalPackages...), special: specialFeatures, } f.known.Store(known) f.enabled.Store(map[Feature]bool{}) f.enabledRaw.Store(map[string]bool{}) f.emulationVersion.Store(emulationVersion) f.queriedFeatures.Store(sets.Set[Feature]{}) klog.V(1).Infof("new feature gate with emulationVersion=%s", f.emulationVersion.Load().String()) return f } // NewFeatureGate creates a feature gate with the current binary version. func NewFeatureGate() *featureGate { binaryVersison := version.MustParse(baseversion.DefaultKubeBinaryVersion) return NewVersionedFeatureGate(binaryVersison) } // Set parses a string of the form "key1=value1,key2=value2,..." into a // map[string]bool of known keys or returns an error. func (f *featureGate) Set(value string) error { m := make(map[string]bool) for _, s := range strings.Split(value, ",") { if len(s) == 0 { continue } arr := strings.SplitN(s, "=", 2) k := strings.TrimSpace(arr[0]) if len(arr) != 2 { return fmt.Errorf("missing bool value for %s", k) } v := strings.TrimSpace(arr[1]) boolValue, err := strconv.ParseBool(v) if err != nil { return fmt.Errorf("invalid value of %s=%s, err: %v", k, v, err) } m[k] = boolValue } return f.SetFromMap(m) } // Validate checks if the flag gates are valid at the emulated version. func (f *featureGate) Validate() []error { f.lock.Lock() defer f.lock.Unlock() m, ok := f.enabledRaw.Load().(map[string]bool) if !ok { return []error{fmt.Errorf("cannot cast enabledRaw to map[string]bool")} } enabled := map[Feature]bool{} return f.unsafeSetFromMap(enabled, m, f.EmulationVersion()) } // unsafeSetFromMap stores flag gates for known features from a map[string]bool into an enabled map. func (f *featureGate) unsafeSetFromMap(enabled map[Feature]bool, m map[string]bool, emulationVersion *version.Version) []error { var errs []error // Copy existing state known := map[Feature]VersionedSpecs{} for k, v := range f.known.Load().(map[Feature]VersionedSpecs) { sort.Sort(v) known[k] = v } for k, v := range m { key := Feature(k) versionedSpecs, ok := known[key] if !ok { // early return if encounters an unknown feature. errs = append(errs, fmt.Errorf("unrecognized feature gate: %s", k)) return errs } featureSpec := featureSpecAtEmulationVersion(versionedSpecs, emulationVersion) if featureSpec.LockToDefault && featureSpec.Default != v { errs = append(errs, fmt.Errorf("cannot set feature gate %v to %v, feature is locked to %v", k, v, featureSpec.Default)) continue } // Handle "special" features like "all alpha gates" if fn, found := f.special[key]; found { fn(known, enabled, v, emulationVersion) enabled[key] = v continue } if featureSpec.PreRelease == PreAlpha { errs = append(errs, fmt.Errorf("cannot set feature gate %v to %v, feature is PreAlpha at emulated version %s", k, v, emulationVersion.String())) continue } enabled[key] = v if featureSpec.PreRelease == Deprecated { klog.Warningf("Setting deprecated feature gate %s=%t. It will be removed in a future release.", k, v) } else if featureSpec.PreRelease == GA { klog.Warningf("Setting GA feature gate %s=%t. It will be removed in a future release.", k, v) } } return errs } // SetFromMap stores flag gates for known features from a map[string]bool or returns an error func (f *featureGate) SetFromMap(m map[string]bool) error { f.lock.Lock() defer f.lock.Unlock() // Copy existing state enabled := map[Feature]bool{} for k, v := range f.enabled.Load().(map[Feature]bool) { enabled[k] = v } enabledRaw := map[string]bool{} for k, v := range f.enabledRaw.Load().(map[string]bool) { enabledRaw[k] = v } // Update enabledRaw first. // SetFromMap might be called when emulationVersion is not finalized yet, and we do not know the final state of enabled. // But the flags still need to be saved. for k, v := range m { enabledRaw[k] = v } f.enabledRaw.Store(enabledRaw) errs := f.unsafeSetFromMap(enabled, enabledRaw, f.EmulationVersion()) if len(errs) == 0 { // Persist changes f.enabled.Store(enabled) klog.V(1).Infof("feature gates: %v", f.enabled) } return utilerrors.NewAggregate(errs) } // String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,...". func (f *featureGate) String() string { pairs := []string{} for k, v := range f.enabled.Load().(map[Feature]bool) { pairs = append(pairs, fmt.Sprintf("%s=%t", k, v)) } sort.Strings(pairs) return strings.Join(pairs, ",") } func (f *featureGate) Type() string { return "mapStringBool" } // Add adds features to the featureGate. func (f *featureGate) Add(features map[Feature]FeatureSpec) error { vs := map[Feature]VersionedSpecs{} for name, spec := range features { // if no version is provided for the FeatureSpec, it is defaulted to version 0.0 so that it can be enabled/disabled regardless of emulation version. spec.Version = version.MajorMinor(0, 0) vs[name] = VersionedSpecs{spec} } return f.AddVersioned(vs) } // AddVersioned adds versioned feature specs to the featureGate. func (f *featureGate) AddVersioned(features map[Feature]VersionedSpecs) error { f.lock.Lock() defer f.lock.Unlock() if f.closed { return fmt.Errorf("cannot add a feature gate after adding it to the flag set") } // Copy existing state known := f.GetAllVersioned() for name, specs := range features { sort.Sort(specs) if existingSpec, found := known[name]; found { sort.Sort(existingSpec) if reflect.DeepEqual(existingSpec, specs) { continue } return fmt.Errorf("feature gate %q with different spec already exists: %v", name, existingSpec) } known[name] = specs } // Persist updated state f.known.Store(known) return nil } func (f *featureGate) OverrideDefault(name Feature, override bool) error { return f.OverrideDefaultAtVersion(name, override, f.EmulationVersion()) } func (f *featureGate) OverrideDefaultAtVersion(name Feature, override bool, ver *version.Version) error { f.lock.Lock() defer f.lock.Unlock() if f.closed { return fmt.Errorf("cannot override default for feature %q: gates already added to a flag set", name) } // Copy existing state known := f.GetAllVersioned() specs, ok := known[name] if !ok { return fmt.Errorf("cannot override default: feature %q is not registered", name) } spec := featureSpecAtEmulationVersion(specs, ver) switch { case spec.LockToDefault: return fmt.Errorf("cannot override default: feature %q default is locked to %t", name, spec.Default) case spec.PreRelease == PreAlpha: return fmt.Errorf("cannot override default: feature %q is not available before version %s", name, ver.String()) case spec.PreRelease == Deprecated: klog.Warningf("Overriding default of deprecated feature gate %s=%t. It will be removed in a future release.", name, override) case spec.PreRelease == GA: klog.Warningf("Overriding default of GA feature gate %s=%t. It will be removed in a future release.", name, override) } spec.Default = override known[name] = specs f.known.Store(known) return nil } // GetAll returns a copy of the map of known feature names to feature specs for the current emulationVersion. func (f *featureGate) GetAll() map[Feature]FeatureSpec { retval := map[Feature]FeatureSpec{} f.lock.Lock() versionedSpecs := f.GetAllVersioned() emuVer := f.EmulationVersion() f.lock.Unlock() for k, v := range versionedSpecs { spec := featureSpecAtEmulationVersion(v, emuVer) if spec.PreRelease == PreAlpha { // The feature is not available at the emulation version. continue } retval[k] = *spec } return retval } // GetAllVersioned returns a copy of the map of known feature names to versioned feature specs. func (f *featureGate) GetAllVersioned() map[Feature]VersionedSpecs { retval := map[Feature]VersionedSpecs{} for k, v := range f.known.Load().(map[Feature]VersionedSpecs) { vCopy := make([]FeatureSpec, len(v)) _ = copy(vCopy, v) retval[k] = vCopy } return retval } func (f *featureGate) SetEmulationVersion(emulationVersion *version.Version) error { if emulationVersion.EqualTo(f.EmulationVersion()) { return nil } f.lock.Lock() defer f.lock.Unlock() klog.V(1).Infof("set feature gate emulationVersion to %s", emulationVersion.String()) // Copy existing state enabledRaw := map[string]bool{} for k, v := range f.enabledRaw.Load().(map[string]bool) { enabledRaw[k] = v } // enabled map should be reset whenever emulationVersion is changed. enabled := map[Feature]bool{} errs := f.unsafeSetFromMap(enabled, enabledRaw, emulationVersion) queriedFeatures := f.queriedFeatures.Load().(sets.Set[Feature]) known := f.known.Load().(map[Feature]VersionedSpecs) for feature := range queriedFeatures { newVal := featureEnabled(feature, enabled, known, emulationVersion) oldVal := featureEnabled(feature, f.enabled.Load().(map[Feature]bool), known, f.EmulationVersion()) if newVal != oldVal { klog.Warningf("SetEmulationVersion will change already queried feature:%s from %v to %v", feature, oldVal, newVal) } } if len(errs) == 0 { // Persist changes f.enabled.Store(enabled) f.emulationVersion.Store(emulationVersion) f.queriedFeatures.Store(sets.Set[Feature]{}) } return utilerrors.NewAggregate(errs) } func (f *featureGate) EmulationVersion() *version.Version { return f.emulationVersion.Load() } // featureSpec returns the featureSpec at the EmulationVersion if the key exists, an error otherwise. // This is useful to keep multiple implementations of a feature based on the PreRelease or Version info. func (f *featureGate) featureSpec(key Feature) (FeatureSpec, error) { if v, ok := f.known.Load().(map[Feature]VersionedSpecs)[key]; ok { featureSpec := f.featureSpecAtEmulationVersion(v) return *featureSpec, nil } return FeatureSpec{}, fmt.Errorf("feature %q is not registered in FeatureGate %q", key, f.featureGateName) } func (f *featureGate) unsafeRecordQueried(key Feature) { queriedFeatures := f.queriedFeatures.Load().(sets.Set[Feature]) if _, ok := queriedFeatures[key]; ok { return } // Clone items from queriedFeatures before mutating it newQueriedFeatures := queriedFeatures.Clone() newQueriedFeatures.Insert(key) f.queriedFeatures.Store(newQueriedFeatures) } func featureEnabled(key Feature, enabled map[Feature]bool, known map[Feature]VersionedSpecs, emulationVersion *version.Version) bool { // check explicitly set enabled list if v, ok := enabled[key]; ok { return v } if v, ok := known[key]; ok { return featureSpecAtEmulationVersion(v, emulationVersion).Default } panic(fmt.Errorf("feature %q is not registered in FeatureGate", key)) } // Enabled returns true if the key is enabled. If the key is not known, this call will panic. func (f *featureGate) Enabled(key Feature) bool { // TODO: ideally we should lock the feature gate in this call to be safe, need to evaluate how much performance impact locking would have. v := featureEnabled(key, f.enabled.Load().(map[Feature]bool), f.known.Load().(map[Feature]VersionedSpecs), f.EmulationVersion()) f.unsafeRecordQueried(key) return v } func (f *featureGate) featureSpecAtEmulationVersion(v VersionedSpecs) *FeatureSpec { return featureSpecAtEmulationVersion(v, f.EmulationVersion()) } func featureSpecAtEmulationVersion(v VersionedSpecs, emulationVersion *version.Version) *FeatureSpec { i := len(v) - 1 for ; i >= 0; i-- { if v[i].Version.GreaterThan(emulationVersion) { continue } return &v[i] } return &FeatureSpec{ Default: false, PreRelease: PreAlpha, Version: version.MajorMinor(0, 0), } } // Close sets closed to true, and prevents subsequent calls to Add func (f *featureGate) Close() { f.lock.Lock() f.closed = true f.lock.Unlock() } // AddFlag adds a flag for setting global feature gates to the specified FlagSet. func (f *featureGate) AddFlag(fs *pflag.FlagSet) { // TODO(mtaufen): Shouldn't we just close it on the first Set/SetFromMap instead? // Not all components expose a feature gates flag using this AddFlag method, and // in the future, all components will completely stop exposing a feature gates flag, // in favor of componentconfig. f.Close() known := f.KnownFeatures() fs.Var(f, flagName, ""+ "A set of key=value pairs that describe feature gates for alpha/experimental features. "+ "Options are:\n"+strings.Join(known, "\n")) } func (f *featureGate) AddMetrics() { for feature, featureSpec := range f.GetAll() { featuremetrics.RecordFeatureInfo(context.Background(), string(feature), string(featureSpec.PreRelease), f.Enabled(feature)) } } // KnownFeatures returns a slice of strings describing the FeatureGate's known features. // preAlpha, Deprecated and GA features are hidden from the list. func (f *featureGate) KnownFeatures() []string { var known []string for k, v := range f.known.Load().(map[Feature]VersionedSpecs) { if k == "AllAlpha" || k == "AllBeta" { known = append(known, fmt.Sprintf("%s=true|false (%s - default=%t)", k, v[0].PreRelease, v[0].Default)) continue } featureSpec := f.featureSpecAtEmulationVersion(v) if featureSpec.PreRelease == GA || featureSpec.PreRelease == Deprecated || featureSpec.PreRelease == PreAlpha { continue } known = append(known, fmt.Sprintf("%s=true|false (%s - default=%t)", k, featureSpec.PreRelease, featureSpec.Default)) } sort.Strings(known) return known } // DeepCopyAndReset copies all the registered features of the FeatureGate object, with all the known features and overrides, // and resets all the enabled status of the new feature gate. // This is useful for creating a new instance of feature gate without inheriting all the enabled configurations of the base feature gate. func (f *featureGate) DeepCopyAndReset() MutableVersionedFeatureGate { fg := NewVersionedFeatureGate(f.EmulationVersion()) known := f.GetAllVersioned() fg.known.Store(known) return fg } // DeepCopy returns a deep copy of the FeatureGate object, such that gates can be // set on the copy without mutating the original. This is useful for validating // config against potential feature gate changes before committing those changes. func (f *featureGate) DeepCopy() MutableVersionedFeatureGate { f.lock.Lock() defer f.lock.Unlock() // Copy existing state. known := f.GetAllVersioned() enabled := map[Feature]bool{} for k, v := range f.enabled.Load().(map[Feature]bool) { enabled[k] = v } enabledRaw := map[string]bool{} for k, v := range f.enabledRaw.Load().(map[string]bool) { enabledRaw[k] = v } // Construct a new featureGate around the copied state. // Note that specialFeatures is treated as immutable by convention, // and we maintain the value of f.closed across the copy. fg := &featureGate{ special: specialFeatures, closed: f.closed, } fg.emulationVersion.Store(f.EmulationVersion()) fg.known.Store(known) fg.enabled.Store(enabled) fg.enabledRaw.Store(enabledRaw) fg.queriedFeatures.Store(sets.Set[Feature]{}) return fg } // ExplicitlySet returns true if the feature value is explicitly set instead of // being derived from the default values or special features. func (f *featureGate) ExplicitlySet(name Feature) bool { enabledRaw := f.enabledRaw.Load().(map[string]bool) _, ok := enabledRaw[string(name)] return ok } // ResetFeatureValueToDefault resets the value of the feature back to the default value. func (f *featureGate) ResetFeatureValueToDefault(name Feature) error { f.lock.Lock() defer f.lock.Unlock() enabled := map[Feature]bool{} for k, v := range f.enabled.Load().(map[Feature]bool) { enabled[k] = v } enabledRaw := map[string]bool{} for k, v := range f.enabledRaw.Load().(map[string]bool) { enabledRaw[k] = v } _, inEnabled := enabled[name] if inEnabled { delete(enabled, name) } _, inEnabledRaw := enabledRaw[string(name)] if inEnabledRaw { delete(enabledRaw, string(name)) } // some features could be in enabled map but not enabledRaw map, // for example some Alpha feature when AllAlpha is set. if inEnabledRaw && !inEnabled { return fmt.Errorf("feature:%s was explicitly set, but not in enabled map", name) } f.enabled.Store(enabled) f.enabledRaw.Store(enabledRaw) return nil } kubernetes-component-base-1b2882b/featuregate/feature_gate_test.go000066400000000000000000001545001476422213000254460ustar00rootroot00000000000000/* Copyright 2016 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 featuregate import ( "fmt" "reflect" "sort" "strings" "testing" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/version" "k8s.io/component-base/metrics/legacyregistry" featuremetrics "k8s.io/component-base/metrics/prometheus/feature" "k8s.io/component-base/metrics/testutil" ) func TestFeatureGateFlag(t *testing.T) { // gates for testing const testAlphaGate Feature = "TestAlpha" const testBetaGate Feature = "TestBeta" const testDeprecatedGate Feature = "TestDeprecated" const testLockedFalseGate Feature = "TestLockedFalse" tests := []struct { arg string expect map[Feature]bool parseError string }{ { arg: "", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: false, testDeprecatedGate: false, testLockedFalseGate: false, }, }, { arg: "TestDeprecated=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: false, testDeprecatedGate: true, testLockedFalseGate: false, }, }, { arg: "TestLockedFalse=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, }, parseError: "cannot set feature gate TestLockedFalse to true, feature is locked to false", }, { arg: "fooBarBaz=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, }, parseError: "unrecognized feature gate: fooBarBaz", }, { arg: "AllAlpha=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, }, }, { arg: "AllAlpha=true", expect: map[Feature]bool{ allAlphaGate: true, allBetaGate: false, testAlphaGate: true, testBetaGate: false, testLockedFalseGate: false, }, }, { arg: "AllAlpha=banana", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, }, parseError: "invalid value of AllAlpha", }, { arg: "AllAlpha=false,TestAlpha=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: true, testBetaGate: false, testLockedFalseGate: false, }, }, { arg: "TestAlpha=true,AllAlpha=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: true, testBetaGate: false, testLockedFalseGate: false, }, }, { arg: "AllAlpha=true,TestAlpha=false", expect: map[Feature]bool{ allAlphaGate: true, allBetaGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, }, }, { arg: "TestAlpha=false,AllAlpha=true", expect: map[Feature]bool{ allAlphaGate: true, allBetaGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, }, }, { arg: "TestBeta=true,AllAlpha=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: true, testLockedFalseGate: false, }, }, { arg: "AllBeta=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: false, }, }, { arg: "AllBeta=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: true, testAlphaGate: false, testBetaGate: true, testLockedFalseGate: false, }, }, { arg: "AllBeta=banana", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, }, parseError: "invalid value of AllBeta", }, { arg: "AllBeta=false,TestBeta=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: true, testLockedFalseGate: false, }, }, { arg: "TestBeta=true,AllBeta=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: true, testLockedFalseGate: false, }, }, { arg: "AllBeta=true,TestBeta=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: true, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, }, }, { arg: "TestBeta=false,AllBeta=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: true, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, }, }, { arg: "TestAlpha=true,AllBeta=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: true, testBetaGate: false, testLockedFalseGate: false, }, }, } for i, test := range tests { t.Run(test.arg, func(t *testing.T) { fs := pflag.NewFlagSet("testfeaturegateflag", pflag.ContinueOnError) f := NewFeatureGate() err := f.Add(map[Feature]FeatureSpec{ testAlphaGate: {Default: false, PreRelease: Alpha}, testBetaGate: {Default: false, PreRelease: Beta}, testDeprecatedGate: {Default: false, PreRelease: Deprecated}, testLockedFalseGate: {Default: false, PreRelease: GA, LockToDefault: true}, }) require.NoError(t, err) f.AddFlag(fs) err = fs.Parse([]string{fmt.Sprintf("--%s=%s", flagName, test.arg)}) if test.parseError != "" { if !strings.Contains(err.Error(), test.parseError) { t.Errorf("%d: Parse() Expected %v, Got %v", i, test.parseError, err) } } else if err != nil { t.Errorf("%d: Parse() Expected nil, Got %v", i, err) } for k, v := range test.expect { if actual := f.enabled.Load().(map[Feature]bool)[k]; actual != v { t.Errorf("%d: expected %s=%v, Got %v", i, k, v, actual) } } }) } } func TestFeatureGateOverride(t *testing.T) { const testAlphaGate Feature = "TestAlpha" const testBetaGate Feature = "TestBeta" // Don't parse the flag, assert defaults are used. var f *featureGate = NewFeatureGate() err := f.Add(map[Feature]FeatureSpec{ testAlphaGate: {Default: false, PreRelease: Alpha}, testBetaGate: {Default: false, PreRelease: Beta}, }) require.NoError(t, err) f.Set("TestAlpha=true,TestBeta=true") if errs := f.Validate(); len(errs) > 0 { t.Fatalf("Validate() Expected no error, Got %v", errs) } if f.Enabled(testAlphaGate) != true { t.Errorf("Expected true") } if f.Enabled(testBetaGate) != true { t.Errorf("Expected true") } f.Set("TestAlpha=false") if errs := f.Validate(); len(errs) > 0 { t.Fatalf("Validate() Expected no error, Got %v", errs) } if f.Enabled(testAlphaGate) != false { t.Errorf("Expected false") } if f.Enabled(testBetaGate) != true { t.Errorf("Expected true") } } func TestFeatureGateFlagDefaults(t *testing.T) { // gates for testing const testAlphaGate Feature = "TestAlpha" const testBetaGate Feature = "TestBeta" // Don't parse the flag, assert defaults are used. var f *featureGate = NewFeatureGate() err := f.Add(map[Feature]FeatureSpec{ testAlphaGate: {Default: false, PreRelease: Alpha}, testBetaGate: {Default: true, PreRelease: Beta}, }) require.NoError(t, err) if f.Enabled(testAlphaGate) != false { t.Errorf("Expected false") } if f.Enabled(testBetaGate) != true { t.Errorf("Expected true") } } func TestFeatureGateKnownFeatures(t *testing.T) { // gates for testing const ( testAlphaGate Feature = "TestAlpha" testBetaGate Feature = "TestBeta" testGAGate Feature = "TestGA" testDeprecatedGate Feature = "TestDeprecated" ) // Don't parse the flag, assert defaults are used. var f *featureGate = NewFeatureGate() err := f.Add(map[Feature]FeatureSpec{ testAlphaGate: {Default: false, PreRelease: Alpha}, testBetaGate: {Default: true, PreRelease: Beta}, testGAGate: {Default: true, PreRelease: GA}, testDeprecatedGate: {Default: false, PreRelease: Deprecated}, }) require.NoError(t, err) known := strings.Join(f.KnownFeatures(), " ") assert.Contains(t, known, testAlphaGate) assert.Contains(t, known, testBetaGate) assert.NotContains(t, known, testGAGate) assert.NotContains(t, known, testDeprecatedGate) } func TestFeatureGateSetFromMap(t *testing.T) { // gates for testing const testAlphaGate Feature = "TestAlpha" const testBetaGate Feature = "TestBeta" const testLockedTrueGate Feature = "TestLockedTrue" const testLockedFalseGate Feature = "TestLockedFalse" tests := []struct { name string setmap map[string]bool expect map[Feature]bool setmapError string }{ { name: "set TestAlpha and TestBeta true", setmap: map[string]bool{ "TestAlpha": true, "TestBeta": true, }, expect: map[Feature]bool{ testAlphaGate: true, testBetaGate: true, }, }, { name: "set TestBeta true", setmap: map[string]bool{ "TestBeta": true, }, expect: map[Feature]bool{ testAlphaGate: false, testBetaGate: true, }, }, { name: "set TestAlpha false", setmap: map[string]bool{ "TestAlpha": false, }, expect: map[Feature]bool{ testAlphaGate: false, testBetaGate: false, }, }, { name: "set TestInvaild true", setmap: map[string]bool{ "TestInvaild": true, }, expect: map[Feature]bool{ testAlphaGate: false, testBetaGate: false, }, setmapError: "unrecognized feature gate:", }, { name: "set locked gates", setmap: map[string]bool{ "TestLockedTrue": true, "TestLockedFalse": false, }, expect: map[Feature]bool{ testAlphaGate: false, testBetaGate: false, }, }, { name: "set locked gates", setmap: map[string]bool{ "TestLockedTrue": false, }, expect: map[Feature]bool{ testAlphaGate: false, testBetaGate: false, }, setmapError: "cannot set feature gate TestLockedTrue to false, feature is locked to true", }, { name: "set locked gates", setmap: map[string]bool{ "TestLockedFalse": true, }, expect: map[Feature]bool{ testAlphaGate: false, testBetaGate: false, }, setmapError: "cannot set feature gate TestLockedFalse to true, feature is locked to false", }, } for i, test := range tests { t.Run(fmt.Sprintf("SetFromMap %s", test.name), func(t *testing.T) { f := NewFeatureGate() err := f.Add(map[Feature]FeatureSpec{ testAlphaGate: {Default: false, PreRelease: Alpha}, testBetaGate: {Default: false, PreRelease: Beta}, testLockedTrueGate: {Default: true, PreRelease: GA, LockToDefault: true}, testLockedFalseGate: {Default: false, PreRelease: GA, LockToDefault: true}, }) require.NoError(t, err) err = f.SetFromMap(test.setmap) if test.setmapError != "" { if err == nil { t.Errorf("expected error, got none") } else if !strings.Contains(err.Error(), test.setmapError) { t.Errorf("%d: SetFromMap(%#v) Expected err:%v, Got err:%v", i, test.setmap, test.setmapError, err) } } else if err != nil { t.Errorf("%d: SetFromMap(%#v) Expected success, Got err:%v", i, test.setmap, err) } for k, v := range test.expect { if actual := f.Enabled(k); actual != v { t.Errorf("%d: SetFromMap(%#v) Expected %s=%v, Got %s=%v", i, test.setmap, k, v, k, actual) } } }) } } func TestFeatureGateMetrics(t *testing.T) { // gates for testing featuremetrics.ResetFeatureInfoMetric() const testAlphaGate Feature = "TestAlpha" const testBetaGate Feature = "TestBeta" const testAlphaEnabled Feature = "TestAlphaEnabled" const testBetaDisabled Feature = "TestBetaDisabled" testedMetrics := []string{"kubernetes_feature_enabled"} expectedOutput := ` # HELP kubernetes_feature_enabled [BETA] This metric records the data about the stage and enablement of a k8s feature. # TYPE kubernetes_feature_enabled gauge kubernetes_feature_enabled{name="TestAlpha",stage="ALPHA"} 0 kubernetes_feature_enabled{name="TestBeta",stage="BETA"} 1 kubernetes_feature_enabled{name="TestAlphaEnabled",stage="ALPHA"} 1 kubernetes_feature_enabled{name="AllAlpha",stage="ALPHA"} 0 kubernetes_feature_enabled{name="AllBeta",stage="BETA"} 0 kubernetes_feature_enabled{name="TestBetaDisabled",stage="ALPHA"} 0 ` f := NewFeatureGate() fMap := map[Feature]FeatureSpec{ testAlphaGate: {Default: false, PreRelease: Alpha}, testAlphaEnabled: {Default: false, PreRelease: Alpha}, testBetaGate: {Default: true, PreRelease: Beta}, testBetaDisabled: {Default: true, PreRelease: Alpha}, } require.NoError(t, f.Add(fMap)) require.NoError(t, f.SetFromMap(map[string]bool{"TestAlphaEnabled": true, "TestBetaDisabled": false})) f.AddMetrics() if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedOutput), testedMetrics...); err != nil { t.Fatal(err) } } func TestFeatureGateString(t *testing.T) { // gates for testing const testAlphaGate Feature = "TestAlpha" const testBetaGate Feature = "TestBeta" const testGAGate Feature = "TestGA" featuremap := map[Feature]FeatureSpec{ testGAGate: {Default: true, PreRelease: GA}, testAlphaGate: {Default: false, PreRelease: Alpha}, testBetaGate: {Default: true, PreRelease: Beta}, } tests := []struct { setmap map[string]bool expect string }{ { setmap: map[string]bool{ "TestAlpha": false, }, expect: "TestAlpha=false", }, { setmap: map[string]bool{ "TestAlpha": false, "TestBeta": true, }, expect: "TestAlpha=false,TestBeta=true", }, { setmap: map[string]bool{ "TestGA": true, "TestAlpha": false, "TestBeta": true, }, expect: "TestAlpha=false,TestBeta=true,TestGA=true", }, } for i, test := range tests { t.Run(fmt.Sprintf("SetFromMap %s", test.expect), func(t *testing.T) { f := NewFeatureGate() require.NoError(t, f.Add(featuremap)) require.NoError(t, f.SetFromMap(test.setmap)) result := f.String() if result != test.expect { t.Errorf("%d: SetFromMap(%#v) Expected %s, Got %s", i, test.setmap, test.expect, result) } }) } } func TestFeatureGateOverrideDefault(t *testing.T) { t.Run("overrides take effect", func(t *testing.T) { f := NewFeatureGate() if err := f.Add(map[Feature]FeatureSpec{ "TestFeature1": {Default: true}, "TestFeature2": {Default: false}, }); err != nil { t.Fatal(err) } require.NoError(t, f.OverrideDefault("TestFeature1", false)) require.NoError(t, f.OverrideDefault("TestFeature2", true)) if f.Enabled("TestFeature1") { t.Error("expected TestFeature1 to have effective default of false") } if !f.Enabled("TestFeature2") { t.Error("expected TestFeature2 to have effective default of true") } }) t.Run("overrides are preserved across deep copies", func(t *testing.T) { f := NewFeatureGate() require.NoError(t, f.Add(map[Feature]FeatureSpec{"TestFeature": {Default: false}})) require.NoError(t, f.OverrideDefault("TestFeature", true)) fcopy := f.DeepCopy() if !fcopy.Enabled("TestFeature") { t.Error("default override was not preserved by deep copy") } }) t.Run("overrides are preserved across CopyKnownFeatures", func(t *testing.T) { f := NewFeatureGate() require.NoError(t, f.Add(map[Feature]FeatureSpec{"TestFeature": {Default: false}})) require.NoError(t, f.OverrideDefault("TestFeature", true)) fcopy := f.DeepCopyAndReset() if !f.Enabled("TestFeature") { t.Error("TestFeature should be enabled by override") } if !fcopy.Enabled("TestFeature") { t.Error("default override was not preserved by CopyKnownFeatures") } }) t.Run("overrides are not passed over after CopyKnownFeatures", func(t *testing.T) { f := NewFeatureGate() require.NoError(t, f.Add(map[Feature]FeatureSpec{"TestFeature": {Default: false}})) fcopy := f.DeepCopyAndReset() require.NoError(t, f.OverrideDefault("TestFeature", true)) if !f.Enabled("TestFeature") { t.Error("TestFeature should be enabled by override") } if fcopy.Enabled("TestFeature") { t.Error("default override should not be passed over after CopyKnownFeatures") } }) t.Run("reflected in known features", func(t *testing.T) { f := NewFeatureGate() if err := f.Add(map[Feature]FeatureSpec{"TestFeature": { Default: false, PreRelease: Alpha, }}); err != nil { t.Fatal(err) } require.NoError(t, f.OverrideDefault("TestFeature", true)) var found bool for _, s := range f.KnownFeatures() { if !strings.Contains(s, "TestFeature") { continue } found = true if !strings.Contains(s, "default=true") { t.Errorf("expected override of default to be reflected in known feature description %q", s) } } if !found { t.Error("found no entry for TestFeature in known features") } }) t.Run("may not change default for specs with locked defaults", func(t *testing.T) { f := NewFeatureGate() if err := f.Add(map[Feature]FeatureSpec{ "LockedFeature": { Default: true, LockToDefault: true, }, }); err != nil { t.Fatal(err) } if f.OverrideDefault("LockedFeature", false) == nil { t.Error("expected error when attempting to override the default for a feature with a locked default") } if f.OverrideDefault("LockedFeature", true) == nil { t.Error("expected error when attempting to override the default for a feature with a locked default") } }) t.Run("does not supersede explicitly-set value", func(t *testing.T) { f := NewFeatureGate() require.NoError(t, f.Add(map[Feature]FeatureSpec{"TestFeature": {Default: true}})) require.NoError(t, f.OverrideDefault("TestFeature", false)) require.NoError(t, f.SetFromMap(map[string]bool{"TestFeature": true})) if !f.Enabled("TestFeature") { t.Error("expected feature to be effectively enabled despite default override") } }) t.Run("prevents re-registration of feature spec after overriding default", func(t *testing.T) { f := NewFeatureGate() if err := f.Add(map[Feature]FeatureSpec{ "TestFeature": { Default: true, PreRelease: Alpha, }, }); err != nil { t.Fatal(err) } require.NoError(t, f.OverrideDefault("TestFeature", false)) if err := f.Add(map[Feature]FeatureSpec{ "TestFeature": { Default: true, PreRelease: Alpha, }, }); err == nil { t.Error("expected re-registration to return a non-nil error after overriding its default") } }) t.Run("does not allow override for an unknown feature", func(t *testing.T) { f := NewFeatureGate() if err := f.OverrideDefault("TestFeature", true); err == nil { t.Error("expected an error to be returned in attempt to override default for unregistered feature") } }) t.Run("returns error if already added to flag set", func(t *testing.T) { f := NewFeatureGate() fs := pflag.NewFlagSet("test", pflag.ContinueOnError) f.AddFlag(fs) if err := f.OverrideDefault("TestFeature", true); err == nil { t.Error("expected a non-nil error to be returned") } }) } func TestVersionedFeatureGateFlag(t *testing.T) { // gates for testing const testGAGate Feature = "TestGA" const testAlphaGate Feature = "TestAlpha" const testBetaGate Feature = "TestBeta" const testLockedFalseGate Feature = "TestLockedFalse" const testAlphaGateNoVersion Feature = "TestAlphaNoVersion" const testBetaGateNoVersion Feature = "TestBetaNoVersion" tests := []struct { arg string expect map[Feature]bool parseError string }{ { arg: "", expect: map[Feature]bool{ testGAGate: false, allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, }, { arg: "TestLockedFalse=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: true, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, }, { arg: "fooBarBaz=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testGAGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, parseError: "unrecognized feature gate: fooBarBaz", }, { arg: "AllAlpha=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testGAGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, }, { arg: "AllAlpha=true", expect: map[Feature]bool{ allAlphaGate: true, allBetaGate: false, testAlphaGate: false, testGAGate: false, testBetaGate: true, testLockedFalseGate: false, testAlphaGateNoVersion: true, testBetaGateNoVersion: false, }, }, { arg: "AllAlpha=banana", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testGAGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, parseError: "invalid value of AllAlpha", }, { arg: "AllAlpha=false,TestAlpha=true,TestAlphaNoVersion=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testGAGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, testAlphaGateNoVersion: true, testBetaGateNoVersion: false, }, parseError: "cannot set feature gate TestAlpha to true, feature is PreAlpha at emulated version 1.28", }, { arg: "AllAlpha=false,TestAlphaNoVersion=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testGAGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, testAlphaGateNoVersion: true, testBetaGateNoVersion: false, }, }, { arg: "TestAlpha=true,TestAlphaNoVersion=true,AllAlpha=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testGAGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, testAlphaGateNoVersion: true, testBetaGateNoVersion: false, }, parseError: "cannot set feature gate TestAlpha to true, feature is PreAlpha at emulated version 1.28", }, { arg: "AllAlpha=true,TestAlpha=false,TestAlphaNoVersion=false", expect: map[Feature]bool{ allAlphaGate: true, allBetaGate: false, testGAGate: false, testAlphaGate: false, testBetaGate: true, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, parseError: "cannot set feature gate TestAlpha to false, feature is PreAlpha at emulated version 1.28", }, { arg: "AllAlpha=true,TestAlphaNoVersion=false", expect: map[Feature]bool{ allAlphaGate: true, allBetaGate: false, testGAGate: false, testAlphaGate: false, testBetaGate: true, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, }, { arg: "TestAlpha=false,TestAlphaNoVersion=false,AllAlpha=true", expect: map[Feature]bool{ allAlphaGate: true, allBetaGate: false, testGAGate: false, testAlphaGate: false, testBetaGate: true, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, parseError: "cannot set feature gate TestAlpha to false, feature is PreAlpha at emulated version 1.28", }, { arg: "TestBeta=true,TestBetaNoVersion=true,TestGA=true,AllAlpha=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testGAGate: true, testAlphaGate: false, testBetaGate: true, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: true, }, }, { arg: "AllBeta=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testGAGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, }, { arg: "AllBeta=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: true, testGAGate: true, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: true, }, }, { arg: "AllBeta=banana", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testGAGate: false, testAlphaGate: false, testBetaGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, parseError: "invalid value of AllBeta", }, { arg: "AllBeta=false,TestBeta=true,TestBetaNoVersion=true,TestGA=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testGAGate: true, testAlphaGate: false, testBetaGate: true, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: true, }, }, { arg: "TestBeta=true,TestBetaNoVersion=true,AllBeta=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testGAGate: false, testAlphaGate: false, testBetaGate: true, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: true, }, }, { arg: "AllBeta=true,TestBetaNoVersion=false,TestBeta=false,TestGA=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: true, testGAGate: false, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, }, { arg: "TestBeta=false,TestBetaNoVersion=false,AllBeta=true", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: true, testGAGate: true, testAlphaGate: false, testBetaGate: false, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, }, { arg: "TestAlpha=true,AllBeta=false", expect: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testGAGate: false, testAlphaGate: true, testBetaGate: false, testLockedFalseGate: false, testAlphaGateNoVersion: false, testBetaGateNoVersion: false, }, parseError: "cannot set feature gate TestAlpha to true, feature is PreAlpha at emulated version 1.28", }, } for i, test := range tests { t.Run(test.arg, func(t *testing.T) { fs := pflag.NewFlagSet("testfeaturegateflag", pflag.ContinueOnError) f := NewVersionedFeatureGate(version.MustParse("1.29")) if err := f.SetEmulationVersion(version.MustParse("1.28")); err != nil { t.Fatalf("failed to SetEmulationVersion: %v", err) } err := f.AddVersioned(map[Feature]VersionedSpecs{ testGAGate: { {Version: version.MustParse("1.29"), Default: true, PreRelease: GA}, {Version: version.MustParse("1.28"), Default: false, PreRelease: Beta}, {Version: version.MustParse("1.27"), Default: false, PreRelease: Alpha}, }, testAlphaGate: { {Version: version.MustParse("1.29"), Default: false, PreRelease: Alpha}, }, testBetaGate: { {Version: version.MustParse("1.29"), Default: false, PreRelease: Beta}, {Version: version.MustParse("1.28"), Default: false, PreRelease: Alpha}, }, testLockedFalseGate: { {Version: version.MustParse("1.29"), Default: false, PreRelease: GA, LockToDefault: true}, {Version: version.MustParse("1.28"), Default: false, PreRelease: GA}, }, }) require.NoError(t, err) err = f.Add(map[Feature]FeatureSpec{ testAlphaGateNoVersion: {Default: false, PreRelease: Alpha}, testBetaGateNoVersion: {Default: false, PreRelease: Beta}, }) require.NoError(t, err) f.AddFlag(fs) var errs []error err = fs.Parse([]string{fmt.Sprintf("--%s=%s", flagName, test.arg)}) if err != nil { errs = append(errs, err) } err = utilerrors.NewAggregate(errs) if test.parseError != "" { if !strings.Contains(err.Error(), test.parseError) { t.Errorf("%d: Parse() Expected %v, Got %v", i, test.parseError, err) } return } else if err != nil { t.Errorf("%d: Parse() Expected nil, Got %v", i, err) } for k, v := range test.expect { if actual := f.enabled.Load().(map[Feature]bool)[k]; actual != v { t.Errorf("%d: expected %s=%v, Got %v", i, k, v, actual) } } }) } } func TestVersionedFeatureGateOverride(t *testing.T) { const testAlphaGate Feature = "TestAlpha" const testBetaGate Feature = "TestBeta" // Don't parse the flag, assert defaults are used. f := NewVersionedFeatureGate(version.MustParse("1.29")) err := f.AddVersioned(map[Feature]VersionedSpecs{ testAlphaGate: { {Version: version.MustParse("1.29"), Default: false, PreRelease: Alpha}, }, testBetaGate: { {Version: version.MustParse("1.29"), Default: false, PreRelease: Beta}, {Version: version.MustParse("1.28"), Default: false, PreRelease: Alpha}, }, }) require.NoError(t, err) if f.Enabled(testAlphaGate) != false { t.Errorf("Expected false") } if f.Enabled(testBetaGate) != false { t.Errorf("Expected false") } if errs := f.Validate(); len(errs) > 0 { t.Errorf("Expected no errors when emulation version is equal to binary version.") } require.NoError(t, f.Set("TestAlpha=true,TestBeta=true")) if f.Enabled(testAlphaGate) != true { t.Errorf("Expected false") } if f.Enabled(testBetaGate) != true { t.Errorf("Expected true") } require.NoError(t, f.Set("TestAlpha=false")) if f.Enabled(testAlphaGate) != false { t.Errorf("Expected false") } if f.Enabled(testBetaGate) != true { t.Errorf("Expected true") } if errs := f.Validate(); len(errs) > 0 { t.Errorf("Expected no errors when emulation version is equal to binary version.") } if err := f.SetEmulationVersion(version.MustParse("1.28")); err == nil { t.Errorf("Expected errors when emulation version is 1.28.") } } func TestVersionedFeatureGateFlagDefaults(t *testing.T) { // gates for testing const testGAGate Feature = "TestGA" const testAlphaGate Feature = "TestAlpha" const testBetaGate Feature = "TestBeta" // Don't parse the flag, assert defaults are used. f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) err := f.AddVersioned(map[Feature]VersionedSpecs{ testGAGate: { {Version: version.MustParse("1.29"), Default: true, PreRelease: GA}, {Version: version.MustParse("1.27"), Default: true, PreRelease: Beta}, {Version: version.MustParse("1.25"), Default: true, PreRelease: Alpha}, }, testAlphaGate: { {Version: version.MustParse("1.29"), Default: false, PreRelease: Alpha}, }, testBetaGate: { {Version: version.MustParse("1.29"), Default: true, PreRelease: Beta}, {Version: version.MustParse("1.28"), Default: false, PreRelease: Beta}, {Version: version.MustParse("1.26"), Default: false, PreRelease: Alpha}, }, }) require.NoError(t, err) if f.Enabled(testAlphaGate) != false { t.Errorf("Expected false") } if fs, _ := f.featureSpec(testAlphaGate); fs.PreRelease != PreAlpha || fs.Version.String() != "0.0" { t.Errorf("Expected (PreAlpha, 0.0)") } if f.Enabled(testBetaGate) != false { t.Errorf("Expected false") } if fs, _ := f.featureSpec(testBetaGate); fs.PreRelease != Beta || fs.Version.String() != "1.28" { t.Errorf("Expected (Beta, 1.28)") } if f.Enabled(testGAGate) != true { t.Errorf("Expected true") } if fs, _ := f.featureSpec(testGAGate); fs.PreRelease != Beta || fs.Version.String() != "1.27" { t.Errorf("Expected (Beta, 1.27)") } if _, err := f.featureSpec("NonExist"); err == nil { t.Errorf("Expected Error") } allFeatures := f.GetAll() expectedAllFeatures := []Feature{testGAGate, testBetaGate, allAlphaGate, allBetaGate} if len(allFeatures) != 4 { t.Errorf("Expected 4 features from GetAll(), got %d", len(allFeatures)) } for _, feature := range expectedAllFeatures { if _, ok := allFeatures[feature]; !ok { t.Errorf("Expected feature %s to be in GetAll()", feature) } } } func TestVersionedFeatureGateKnownFeatures(t *testing.T) { // gates for testing const ( testPreAlphaGate Feature = "TestPreAlpha" testAlphaGate Feature = "TestAlpha" testBetaGate Feature = "TestBeta" testGAGate Feature = "TestGA" testDeprecatedGate Feature = "TestDeprecated" testGAGateNoVersion Feature = "TestGANoVersion" testAlphaGateNoVersion Feature = "TestAlphaNoVersion" testBetaGateNoVersion Feature = "TestBetaNoVersion" testDeprecatedGateNoVersion Feature = "TestDeprecatedNoVersion" ) // Don't parse the flag, assert defaults are used. f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) err := f.AddVersioned(map[Feature]VersionedSpecs{ testGAGate: { {Version: version.MustParse("1.27"), Default: false, PreRelease: Beta}, {Version: version.MustParse("1.28"), Default: true, PreRelease: GA}, }, testPreAlphaGate: { {Version: version.MustParse("1.29"), Default: false, PreRelease: Alpha}, }, testAlphaGate: { {Version: version.MustParse("1.28"), Default: false, PreRelease: Alpha}, }, testBetaGate: { {Version: version.MustParse("1.28"), Default: false, PreRelease: Beta}, }, testDeprecatedGate: { {Version: version.MustParse("1.28"), Default: true, PreRelease: Deprecated}, {Version: version.MustParse("1.26"), Default: false, PreRelease: Alpha}, }, }) require.NoError(t, err) err = f.Add(map[Feature]FeatureSpec{ testAlphaGateNoVersion: {Default: false, PreRelease: Alpha}, testBetaGateNoVersion: {Default: false, PreRelease: Beta}, testGAGateNoVersion: {Default: false, PreRelease: GA}, testDeprecatedGateNoVersion: {Default: false, PreRelease: Deprecated}, }) require.NoError(t, err) known := strings.Join(f.KnownFeatures(), " ") assert.NotContains(t, known, testPreAlphaGate) assert.Contains(t, known, testAlphaGate) assert.Contains(t, known, testBetaGate) assert.NotContains(t, known, testGAGate) assert.NotContains(t, known, testDeprecatedGate) assert.Contains(t, known, testAlphaGateNoVersion) assert.Contains(t, known, testBetaGateNoVersion) assert.NotContains(t, known, testGAGateNoVersion) assert.NotContains(t, known, testDeprecatedGateNoVersion) } func TestVersionedFeatureGateMetrics(t *testing.T) { // gates for testing featuremetrics.ResetFeatureInfoMetric() const testAlphaGate Feature = "TestAlpha" const testBetaGate Feature = "TestBeta" const testAlphaEnabled Feature = "TestAlphaEnabled" const testBetaDisabled Feature = "TestBetaDisabled" testedMetrics := []string{"kubernetes_feature_enabled"} expectedOutput := ` # HELP kubernetes_feature_enabled [BETA] This metric records the data about the stage and enablement of a k8s feature. # TYPE kubernetes_feature_enabled gauge kubernetes_feature_enabled{name="TestAlpha",stage="ALPHA"} 0 kubernetes_feature_enabled{name="TestBeta",stage="BETA"} 1 kubernetes_feature_enabled{name="TestAlphaEnabled",stage="ALPHA"} 1 kubernetes_feature_enabled{name="AllAlpha",stage="ALPHA"} 0 kubernetes_feature_enabled{name="AllBeta",stage="BETA"} 0 kubernetes_feature_enabled{name="TestBetaDisabled",stage="BETA"} 0 ` f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) err := f.AddVersioned(map[Feature]VersionedSpecs{ testAlphaGate: { {Version: version.MustParse("1.28"), Default: false, PreRelease: Alpha}, {Version: version.MustParse("1.29"), Default: true, PreRelease: Beta}, }, testAlphaEnabled: { {Version: version.MustParse("1.28"), Default: false, PreRelease: Alpha}, {Version: version.MustParse("1.29"), Default: true, PreRelease: Beta}, }, testBetaGate: { {Version: version.MustParse("1.28"), Default: true, PreRelease: Beta}, {Version: version.MustParse("1.27"), Default: false, PreRelease: Alpha}, }, testBetaDisabled: { {Version: version.MustParse("1.28"), Default: true, PreRelease: Beta}, {Version: version.MustParse("1.27"), Default: false, PreRelease: Alpha}, }, }) require.NoError(t, err) require.NoError(t, f.SetFromMap(map[string]bool{"TestAlphaEnabled": true, "TestBetaDisabled": false})) f.AddMetrics() if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(expectedOutput), testedMetrics...); err != nil { t.Fatal(err) } } func TestVersionedFeatureGateOverrideDefault(t *testing.T) { t.Run("overrides take effect", func(t *testing.T) { f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) if err := f.AddVersioned(map[Feature]VersionedSpecs{ "TestFeature1": { {Version: version.MustParse("1.28"), Default: true}, }, "TestFeature2": { {Version: version.MustParse("1.26"), Default: false}, {Version: version.MustParse("1.29"), Default: true}, }, }); err != nil { t.Fatal(err) } require.NoError(t, f.OverrideDefault("TestFeature1", false)) require.NoError(t, f.OverrideDefault("TestFeature2", true)) if f.Enabled("TestFeature1") { t.Error("expected TestFeature1 to have effective default of false") } if !f.Enabled("TestFeature2") { t.Error("expected TestFeature2 to have effective default of true") } }) t.Run("overrides at specific version take effect", func(t *testing.T) { f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) if err := f.AddVersioned(map[Feature]VersionedSpecs{ "TestFeature1": { {Version: version.MustParse("1.28"), Default: true}, }, "TestFeature2": { {Version: version.MustParse("1.26"), Default: false}, {Version: version.MustParse("1.29"), Default: false}, }, }); err != nil { t.Fatal(err) } if f.OverrideDefaultAtVersion("TestFeature1", false, version.MustParse("1.27")) == nil { t.Error("expected error when attempting to override the default for a feature not available at given version") } require.NoError(t, f.OverrideDefaultAtVersion("TestFeature2", true, version.MustParse("1.27"))) if !f.Enabled("TestFeature1") { t.Error("expected TestFeature1 to have effective default of true") } if !f.Enabled("TestFeature2") { t.Error("expected TestFeature2 to have effective default of true") } require.NoError(t, f.SetEmulationVersion(version.MustParse("1.29"))) if !f.Enabled("TestFeature1") { t.Error("expected TestFeature1 to have effective default of true") } if f.Enabled("TestFeature2") { t.Error("expected TestFeature2 to have effective default of false") } require.NoError(t, f.SetEmulationVersion(version.MustParse("1.26"))) if f.Enabled("TestFeature1") { t.Error("expected TestFeature1 to have effective default of false") } if !f.Enabled("TestFeature2") { t.Error("expected TestFeature2 to have effective default of true") } }) t.Run("overrides are preserved across deep copies", func(t *testing.T) { f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) if err := f.AddVersioned(map[Feature]VersionedSpecs{ "TestFeature": { {Version: version.MustParse("1.28"), Default: false}, {Version: version.MustParse("1.29"), Default: true}, }, }); err != nil { t.Fatal(err) } require.NoError(t, f.OverrideDefault("TestFeature", true)) fcopy := f.DeepCopy() if !fcopy.Enabled("TestFeature") { t.Error("default override was not preserved by deep copy") } }) t.Run("overrides are not passed over after deep copies", func(t *testing.T) { f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) if err := f.AddVersioned(map[Feature]VersionedSpecs{ "TestFeature": { {Version: version.MustParse("1.28"), Default: false}, {Version: version.MustParse("1.29"), Default: true}, }, }); err != nil { t.Fatal(err) } assert.False(t, f.Enabled("TestFeature")) fcopy := f.DeepCopy() require.NoError(t, f.OverrideDefault("TestFeature", true)) require.NoError(t, f.OverrideDefaultAtVersion("TestFeature", false, version.MustParse("1.29"))) assert.True(t, f.Enabled("TestFeature")) assert.False(t, fcopy.Enabled("TestFeature")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.29"))) assert.False(t, f.Enabled("TestFeature")) assert.False(t, fcopy.Enabled("TestFeature")) require.NoError(t, fcopy.SetEmulationVersion(version.MustParse("1.29"))) assert.False(t, f.Enabled("TestFeature")) assert.True(t, fcopy.Enabled("TestFeature")) }) t.Run("reflected in known features", func(t *testing.T) { f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) if err := f.AddVersioned(map[Feature]VersionedSpecs{ "TestFeature": { {Version: version.MustParse("1.28"), Default: false, PreRelease: Alpha}, {Version: version.MustParse("1.29"), Default: true, PreRelease: Beta}, }, }); err != nil { t.Fatal(err) } require.NoError(t, f.OverrideDefault("TestFeature", true)) var found bool for _, s := range f.KnownFeatures() { if !strings.Contains(s, "TestFeature") { continue } found = true if !strings.Contains(s, "default=true") { t.Errorf("expected override of default to be reflected in known feature description %q", s) } } if !found { t.Error("found no entry for TestFeature in known features") } }) t.Run("may not change default for specs with locked defaults", func(t *testing.T) { f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) if err := f.AddVersioned(map[Feature]VersionedSpecs{ "LockedFeature": { {Version: version.MustParse("1.28"), Default: true, LockToDefault: true}, }, }); err != nil { t.Fatal(err) } if f.OverrideDefault("LockedFeature", false) == nil { t.Error("expected error when attempting to override the default for a feature with a locked default") } if f.OverrideDefault("LockedFeature", true) == nil { t.Error("expected error when attempting to override the default for a feature with a locked default") } }) t.Run("can change default for specs without locked defaults for emulation version", func(t *testing.T) { f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) if err := f.AddVersioned(map[Feature]VersionedSpecs{ "LockedFeature": { {Version: version.MustParse("1.28"), Default: true}, {Version: version.MustParse("1.29"), Default: true, LockToDefault: true}, }, }); err != nil { t.Fatal(err) } require.NoError(t, f.OverrideDefault("LockedFeature", false)) if f.Enabled("LockedFeature") { t.Error("expected LockedFeature to have effective default of false") } }) t.Run("does not supersede explicitly-set value", func(t *testing.T) { f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) if err := f.AddVersioned(map[Feature]VersionedSpecs{ "TestFeature": { {Version: version.MustParse("1.28"), Default: true}, }, }); err != nil { t.Fatal(err) } require.NoError(t, f.OverrideDefault("TestFeature", false)) require.NoError(t, f.SetFromMap(map[string]bool{"TestFeature": true})) if !f.Enabled("TestFeature") { t.Error("expected feature to be effectively enabled despite default override") } }) t.Run("prevents re-registration of feature spec after overriding default", func(t *testing.T) { f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) if err := f.AddVersioned(map[Feature]VersionedSpecs{ "TestFeature": { {Version: version.MustParse("1.28"), Default: true, PreRelease: Alpha}, }, }); err != nil { t.Fatal(err) } require.NoError(t, f.OverrideDefault("TestFeature", false)) if err := f.Add(map[Feature]FeatureSpec{ "TestFeature": { Default: true, PreRelease: Alpha, }, }); err == nil { t.Error("expected re-registration to return a non-nil error after overriding its default") } }) t.Run("does not allow override for a feature added after emulation version", func(t *testing.T) { f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) if err := f.AddVersioned(map[Feature]VersionedSpecs{ "TestFeature": { {Version: version.MustParse("1.29"), Default: false}, }, }); err != nil { t.Fatal(err) } if err := f.OverrideDefault("TestFeature", true); err == nil { t.Error("expected an error to be returned in attempt to override default for a feature added after emulation version") } }) t.Run("does not allow override for an unknown feature", func(t *testing.T) { f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) if err := f.OverrideDefault("TestFeature", true); err == nil { t.Error("expected an error to be returned in attempt to override default for unregistered feature") } }) t.Run("returns error if already added to flag set", func(t *testing.T) { f := NewVersionedFeatureGate(version.MustParse("1.29")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) fs := pflag.NewFlagSet("test", pflag.ContinueOnError) f.AddFlag(fs) if err := f.OverrideDefault("TestFeature", true); err == nil { t.Error("expected a non-nil error to be returned") } }) } func TestFeatureSpecAtEmulationVersion(t *testing.T) { specs := VersionedSpecs{{Version: version.MustParse("1.29"), Default: true, PreRelease: GA}, {Version: version.MustParse("1.28"), Default: false, PreRelease: Beta}, {Version: version.MustParse("1.25"), Default: false, PreRelease: Alpha}, } sort.Sort(specs) tests := []struct { cVersion string expect FeatureSpec }{ { cVersion: "1.30", expect: FeatureSpec{Version: version.MustParse("1.29"), Default: true, PreRelease: GA}, }, { cVersion: "1.29", expect: FeatureSpec{Version: version.MustParse("1.29"), Default: true, PreRelease: GA}, }, { cVersion: "1.28", expect: FeatureSpec{Version: version.MustParse("1.28"), Default: false, PreRelease: Beta}, }, { cVersion: "1.27", expect: FeatureSpec{Version: version.MustParse("1.25"), Default: false, PreRelease: Alpha}, }, { cVersion: "1.25", expect: FeatureSpec{Version: version.MustParse("1.25"), Default: false, PreRelease: Alpha}, }, { cVersion: "1.24", expect: FeatureSpec{Version: version.MajorMinor(0, 0), Default: false, PreRelease: PreAlpha}, }, } for i, test := range tests { t.Run(fmt.Sprintf("featureSpecAtEmulationVersion for emulationVersion %s", test.cVersion), func(t *testing.T) { result := featureSpecAtEmulationVersion(specs, version.MustParse(test.cVersion)) if !reflect.DeepEqual(*result, test.expect) { t.Errorf("%d: featureSpecAtEmulationVersion(, %s) Expected %v, Got %v", i, test.cVersion, test.expect, result) } }) } } func TestCopyKnownFeatures(t *testing.T) { f := NewFeatureGate() require.NoError(t, f.Add(map[Feature]FeatureSpec{"FeatureA": {Default: false}, "FeatureB": {Default: false}})) require.NoError(t, f.Set("FeatureA=true")) require.NoError(t, f.OverrideDefault("FeatureB", true)) fcopy := f.DeepCopyAndReset() require.NoError(t, f.Add(map[Feature]FeatureSpec{"FeatureC": {Default: false}})) assert.True(t, f.Enabled("FeatureA")) assert.True(t, f.Enabled("FeatureB")) assert.False(t, f.Enabled("FeatureC")) assert.False(t, fcopy.Enabled("FeatureA")) assert.True(t, fcopy.Enabled("FeatureB")) require.NoError(t, fcopy.Set("FeatureB=false")) assert.True(t, f.Enabled("FeatureB")) assert.False(t, fcopy.Enabled("FeatureB")) if err := fcopy.Set("FeatureC=true"); err == nil { t.Error("expected FeatureC not registered in the copied feature gate") } } func TestExplicitlySet(t *testing.T) { // gates for testing const testAlphaGate Feature = "TestAlpha" const testBetaGate Feature = "TestBeta" tests := []struct { arg string expectedFeatureValue map[Feature]bool expectedExplicitlySet map[Feature]bool }{ { arg: "", expectedFeatureValue: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: false, }, expectedExplicitlySet: map[Feature]bool{ allAlphaGate: false, allBetaGate: false, testAlphaGate: false, testBetaGate: false, }, }, { arg: "AllAlpha=true,TestBeta=false", expectedFeatureValue: map[Feature]bool{ allAlphaGate: true, allBetaGate: false, testAlphaGate: true, testBetaGate: false, }, expectedExplicitlySet: map[Feature]bool{ allAlphaGate: true, allBetaGate: false, testAlphaGate: false, testBetaGate: true, }, }, { arg: "AllAlpha=true,AllBeta=false", expectedFeatureValue: map[Feature]bool{ allAlphaGate: true, allBetaGate: false, testAlphaGate: true, testBetaGate: false, }, expectedExplicitlySet: map[Feature]bool{ allAlphaGate: true, allBetaGate: true, testAlphaGate: false, testBetaGate: false, }, }, } for i, test := range tests { t.Run(test.arg, func(t *testing.T) { fs := pflag.NewFlagSet("testfeaturegateflag", pflag.ContinueOnError) f := NewVersionedFeatureGate(version.MustParse("1.29")) err := f.AddVersioned(map[Feature]VersionedSpecs{ testAlphaGate: { {Version: version.MustParse("1.29"), Default: false, PreRelease: Alpha}, }, testBetaGate: { {Version: version.MustParse("1.29"), Default: false, PreRelease: Beta}, {Version: version.MustParse("1.28"), Default: false, PreRelease: Alpha}, }, }) require.NoError(t, err) f.AddFlag(fs) var errs []error err = fs.Parse([]string{fmt.Sprintf("--%s=%s", flagName, test.arg)}) if err != nil { errs = append(errs, err) } err = utilerrors.NewAggregate(errs) require.NoError(t, err) for k, v := range test.expectedFeatureValue { if actual := f.Enabled(k); actual != v { t.Errorf("%d: expected %s=%v, Got %v", i, k, v, actual) } } for k, v := range test.expectedExplicitlySet { if actual := f.ExplicitlySet(k); actual != v { t.Errorf("%d: expected ExplicitlySet(%s)=%v, Got %v", i, k, v, actual) } } }) } } func TestResetFeatureValueToDefault(t *testing.T) { // gates for testing const testAlphaGate Feature = "TestAlpha" const testBetaGate Feature = "TestBeta" f := NewVersionedFeatureGate(version.MustParse("1.29")) err := f.AddVersioned(map[Feature]VersionedSpecs{ testAlphaGate: { {Version: version.MustParse("1.29"), Default: false, PreRelease: Alpha}, }, testBetaGate: { {Version: version.MustParse("1.29"), Default: true, PreRelease: Beta}, {Version: version.MustParse("1.28"), Default: false, PreRelease: Alpha}, }, }) require.NoError(t, err) fs := pflag.NewFlagSet("testfeaturegateflag", pflag.ContinueOnError) assert.False(t, f.Enabled("AllAlpha")) assert.False(t, f.Enabled("AllBeta")) assert.False(t, f.Enabled("TestAlpha")) assert.True(t, f.Enabled("TestBeta")) f.AddFlag(fs) var errs []error err = fs.Parse([]string{fmt.Sprintf("--%s=%s", flagName, "AllAlpha=true,TestBeta=false")}) if err != nil { errs = append(errs, err) } err = utilerrors.NewAggregate(errs) require.NoError(t, err) assert.True(t, f.Enabled("AllAlpha")) assert.False(t, f.Enabled("AllBeta")) assert.True(t, f.Enabled("TestAlpha")) assert.False(t, f.Enabled("TestBeta")) require.NoError(t, f.ResetFeatureValueToDefault("AllAlpha")) assert.False(t, f.Enabled("AllAlpha")) assert.False(t, f.Enabled("AllBeta")) assert.True(t, f.Enabled("TestAlpha")) assert.False(t, f.Enabled("TestBeta")) require.NoError(t, f.ResetFeatureValueToDefault("TestBeta")) assert.False(t, f.Enabled("AllAlpha")) assert.False(t, f.Enabled("AllBeta")) assert.True(t, f.Enabled("TestAlpha")) assert.True(t, f.Enabled("TestBeta")) require.NoError(t, f.SetEmulationVersion(version.MustParse("1.28"))) assert.False(t, f.Enabled("AllAlpha")) assert.False(t, f.Enabled("AllBeta")) assert.False(t, f.Enabled("TestAlpha")) assert.False(t, f.Enabled("TestBeta")) } kubernetes-component-base-1b2882b/featuregate/registry.go000066400000000000000000000437251476422213000236320ustar00rootroot00000000000000/* 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 featuregate import ( "fmt" "sort" "strings" "sync" "github.com/spf13/pflag" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/version" cliflag "k8s.io/component-base/cli/flag" baseversion "k8s.io/component-base/version" "k8s.io/klog/v2" ) // DefaultComponentGlobalsRegistry is the global var to store the effective versions and feature gates for all components for easy access. // Example usage: // // register the component effective version and feature gate first // _, _ = utilversion.DefaultComponentGlobalsRegistry.ComponentGlobalsOrRegister(utilversion.DefaultKubeComponent, utilversion.DefaultKubeEffectiveVersion(), utilfeature.DefaultMutableFeatureGate) // wardleEffectiveVersion := utilversion.NewEffectiveVersion("1.2") // wardleFeatureGate := featuregate.NewFeatureGate() // utilruntime.Must(utilversion.DefaultComponentGlobalsRegistry.Register(apiserver.WardleComponentName, wardleEffectiveVersion, wardleFeatureGate, false)) // // cmd := &cobra.Command{ // ... // // call DefaultComponentGlobalsRegistry.Set() in PersistentPreRunE // PersistentPreRunE: func(*cobra.Command, []string) error { // if err := utilversion.DefaultComponentGlobalsRegistry.Set(); err != nil { // return err // } // ... // }, // RunE: func(c *cobra.Command, args []string) error { // // call utilversion.DefaultComponentGlobalsRegistry.Validate() somewhere // }, // } // // flags := cmd.Flags() // // add flags // utilversion.DefaultComponentGlobalsRegistry.AddFlags(flags) var DefaultComponentGlobalsRegistry ComponentGlobalsRegistry = NewComponentGlobalsRegistry() const ( DefaultKubeComponent = "kube" klogLevel = 2 ) type VersionMapping func(from *version.Version) *version.Version // ComponentGlobals stores the global variables for a component for easy access. type ComponentGlobals struct { effectiveVersion baseversion.MutableEffectiveVersion featureGate MutableVersionedFeatureGate // emulationVersionMapping contains the mapping from the emulation version of this component // to the emulation version of another component. emulationVersionMapping map[string]VersionMapping // dependentEmulationVersion stores whether or not this component's EmulationVersion is dependent through mapping on another component. // If true, the emulation version cannot be set from the flag, or version mapping from another component. dependentEmulationVersion bool // minCompatibilityVersionMapping contains the mapping from the min compatibility version of this component // to the min compatibility version of another component. minCompatibilityVersionMapping map[string]VersionMapping // dependentMinCompatibilityVersion stores whether or not this component's MinCompatibilityVersion is dependent through mapping on another component // If true, the min compatibility version cannot be set from the flag, or version mapping from another component. dependentMinCompatibilityVersion bool } type ComponentGlobalsRegistry interface { // EffectiveVersionFor returns the EffectiveVersion registered under the component. // Returns nil if the component is not registered. EffectiveVersionFor(component string) baseversion.EffectiveVersion // FeatureGateFor returns the FeatureGate registered under the component. // Returns nil if the component is not registered. FeatureGateFor(component string) FeatureGate // Register registers the EffectiveVersion and FeatureGate for a component. // returns error if the component is already registered. Register(component string, effectiveVersion baseversion.MutableEffectiveVersion, featureGate MutableVersionedFeatureGate) error // ComponentGlobalsOrRegister would return the registered global variables for the component if it already exists in the registry. // Otherwise, the provided variables would be registered under the component, and the same variables would be returned. ComponentGlobalsOrRegister(component string, effectiveVersion baseversion.MutableEffectiveVersion, featureGate MutableVersionedFeatureGate) (baseversion.MutableEffectiveVersion, MutableVersionedFeatureGate) // AddFlags adds flags of "--emulated-version" and "--feature-gates" AddFlags(fs *pflag.FlagSet) // Set sets the flags for all global variables for all components registered. Set() error // SetFallback calls Set() if it has never been called. SetFallback() error // Validate calls the Validate() function for all the global variables for all components registered. Validate() []error // Reset removes all stored ComponentGlobals, configurations, and version mappings. Reset() // SetEmulationVersionMapping sets the mapping from the emulation version of one component // to the emulation version of another component. // Once set, the emulation version of the toComponent will be determined by the emulation version of the fromComponent, // and cannot be set from cmd flags anymore. // For a given component, its emulation version can only depend on one other component, no multiple dependency is allowed. SetEmulationVersionMapping(fromComponent, toComponent string, f VersionMapping) error } type componentGlobalsRegistry struct { componentGlobals map[string]*ComponentGlobals mutex sync.RWMutex // list of component name to emulation version set from the flag. emulationVersionConfig []string // map of component name to the list of feature gates set from the flag. featureGatesConfig map[string][]string // set stores if the Set() function for the registry is already called. set bool } func NewComponentGlobalsRegistry() *componentGlobalsRegistry { return &componentGlobalsRegistry{ componentGlobals: make(map[string]*ComponentGlobals), emulationVersionConfig: nil, featureGatesConfig: nil, } } func (r *componentGlobalsRegistry) Reset() { r.mutex.Lock() defer r.mutex.Unlock() r.componentGlobals = make(map[string]*ComponentGlobals) r.emulationVersionConfig = nil r.featureGatesConfig = nil r.set = false } func (r *componentGlobalsRegistry) EffectiveVersionFor(component string) baseversion.EffectiveVersion { r.mutex.RLock() defer r.mutex.RUnlock() globals, ok := r.componentGlobals[component] if !ok { return nil } return globals.effectiveVersion } func (r *componentGlobalsRegistry) FeatureGateFor(component string) FeatureGate { r.mutex.RLock() defer r.mutex.RUnlock() globals, ok := r.componentGlobals[component] if !ok { return nil } return globals.featureGate } func (r *componentGlobalsRegistry) unsafeRegister(component string, effectiveVersion baseversion.MutableEffectiveVersion, featureGate MutableVersionedFeatureGate) error { if _, ok := r.componentGlobals[component]; ok { return fmt.Errorf("component globals of %s already registered", component) } if featureGate != nil { if err := featureGate.SetEmulationVersion(effectiveVersion.EmulationVersion()); err != nil { return err } } c := ComponentGlobals{ effectiveVersion: effectiveVersion, featureGate: featureGate, emulationVersionMapping: make(map[string]VersionMapping), minCompatibilityVersionMapping: make(map[string]VersionMapping), } r.componentGlobals[component] = &c return nil } func (r *componentGlobalsRegistry) Register(component string, effectiveVersion baseversion.MutableEffectiveVersion, featureGate MutableVersionedFeatureGate) error { if effectiveVersion == nil { return fmt.Errorf("cannot register nil effectiveVersion") } r.mutex.Lock() defer r.mutex.Unlock() return r.unsafeRegister(component, effectiveVersion, featureGate) } func (r *componentGlobalsRegistry) ComponentGlobalsOrRegister(component string, effectiveVersion baseversion.MutableEffectiveVersion, featureGate MutableVersionedFeatureGate) (baseversion.MutableEffectiveVersion, MutableVersionedFeatureGate) { r.mutex.Lock() defer r.mutex.Unlock() globals, ok := r.componentGlobals[component] if ok { return globals.effectiveVersion, globals.featureGate } utilruntime.Must(r.unsafeRegister(component, effectiveVersion, featureGate)) return effectiveVersion, featureGate } func (r *componentGlobalsRegistry) unsafeKnownFeatures() []string { var known []string for component, globals := range r.componentGlobals { if globals.featureGate == nil { continue } for _, f := range globals.featureGate.KnownFeatures() { known = append(known, component+":"+f) } } sort.Strings(known) return known } func (r *componentGlobalsRegistry) unsafeVersionFlagOptions(isEmulation bool) []string { var vs []string for component, globals := range r.componentGlobals { binaryVer := globals.effectiveVersion.BinaryVersion() if isEmulation { if globals.dependentEmulationVersion { continue } // emulated version could be between binaryMajor.{binaryMinor} and binaryMajor.{binaryMinor} // TODO: change to binaryMajor.{binaryMinor-1} and binaryMajor.{binaryMinor} in 1.32 vs = append(vs, fmt.Sprintf("%s=%s..%s (default=%s)", component, binaryVer.SubtractMinor(0).String(), binaryVer.String(), globals.effectiveVersion.EmulationVersion().String())) } else { if globals.dependentMinCompatibilityVersion { continue } // min compatibility version could be between binaryMajor.{binaryMinor-1} and binaryMajor.{binaryMinor} vs = append(vs, fmt.Sprintf("%s=%s..%s (default=%s)", component, binaryVer.SubtractMinor(1).String(), binaryVer.String(), globals.effectiveVersion.MinCompatibilityVersion().String())) } } sort.Strings(vs) return vs } func (r *componentGlobalsRegistry) AddFlags(fs *pflag.FlagSet) { if r == nil { return } r.mutex.Lock() defer r.mutex.Unlock() for _, globals := range r.componentGlobals { if globals.featureGate != nil { globals.featureGate.Close() } } if r.emulationVersionConfig != nil || r.featureGatesConfig != nil { klog.Warning("calling componentGlobalsRegistry.AddFlags more than once, the registry will be set by the latest flags") } r.emulationVersionConfig = []string{} r.featureGatesConfig = make(map[string][]string) fs.StringSliceVar(&r.emulationVersionConfig, "emulated-version", r.emulationVersionConfig, ""+ "The versions different components emulate their capabilities (APIs, features, ...) of.\n"+ "If set, the component will emulate the behavior of this version instead of the underlying binary version.\n"+ "Version format could only be major.minor, for example: '--emulated-version=wardle=1.2,kube=1.31'. Options are:\n"+strings.Join(r.unsafeVersionFlagOptions(true), "\n")+ "If the component is not specified, defaults to \"kube\"") fs.Var(cliflag.NewColonSeparatedMultimapStringStringAllowDefaultEmptyKey(&r.featureGatesConfig), "feature-gates", "Comma-separated list of component:key=value pairs that describe feature gates for alpha/experimental features of different components.\n"+ "If the component is not specified, defaults to \"kube\". This flag can be repeatedly invoked. For example: --feature-gates 'wardle:featureA=true,wardle:featureB=false' --feature-gates 'kube:featureC=true'"+ "Options are:\n"+strings.Join(r.unsafeKnownFeatures(), "\n")) } type componentVersion struct { component string ver *version.Version } // getFullEmulationVersionConfig expands the given version config with version registered version mapping, // and returns the map of component to Version. func (r *componentGlobalsRegistry) getFullEmulationVersionConfig( versionConfigMap map[string]*version.Version) (map[string]*version.Version, error) { result := map[string]*version.Version{} setQueue := []componentVersion{} for comp, ver := range versionConfigMap { if _, ok := r.componentGlobals[comp]; !ok { return result, fmt.Errorf("component not registered: %s", comp) } klog.V(klogLevel).Infof("setting version %s=%s", comp, ver.String()) setQueue = append(setQueue, componentVersion{comp, ver}) } for len(setQueue) > 0 { cv := setQueue[0] if _, visited := result[cv.component]; visited { return result, fmt.Errorf("setting version of %s more than once, probably version mapping loop", cv.component) } setQueue = setQueue[1:] result[cv.component] = cv.ver for toComp, f := range r.componentGlobals[cv.component].emulationVersionMapping { toVer := f(cv.ver) if toVer == nil { return result, fmt.Errorf("got nil version from mapping of %s=%s to component:%s", cv.component, cv.ver.String(), toComp) } klog.V(klogLevel).Infof("setting version %s=%s from version mapping of %s=%s", toComp, toVer.String(), cv.component, cv.ver.String()) setQueue = append(setQueue, componentVersion{toComp, toVer}) } } return result, nil } func toVersionMap(versionConfig []string) (map[string]*version.Version, error) { m := map[string]*version.Version{} for _, compVer := range versionConfig { // default to "kube" of component is not specified k := "kube" v := compVer if strings.Contains(compVer, "=") { arr := strings.SplitN(compVer, "=", 2) if len(arr) != 2 { return m, fmt.Errorf("malformed pair, expect string=string") } k = strings.TrimSpace(arr[0]) v = strings.TrimSpace(arr[1]) } ver, err := version.Parse(v) if err != nil { return m, err } if ver.Patch() != 0 { return m, fmt.Errorf("patch version not allowed, got: %s=%s", k, ver.String()) } if existingVer, ok := m[k]; ok { return m, fmt.Errorf("duplicate version flag, %s=%s and %s=%s", k, existingVer.String(), k, ver.String()) } m[k] = ver } return m, nil } func (r *componentGlobalsRegistry) SetFallback() error { r.mutex.Lock() set := r.set r.mutex.Unlock() if set { return nil } klog.Warning("setting componentGlobalsRegistry in SetFallback. We recommend calling componentGlobalsRegistry.Set()" + " right after parsing flags to avoid using feature gates before their final values are set by the flags.") return r.Set() } func (r *componentGlobalsRegistry) Set() error { r.mutex.Lock() defer r.mutex.Unlock() r.set = true emulationVersionConfigMap, err := toVersionMap(r.emulationVersionConfig) if err != nil { return err } for comp := range emulationVersionConfigMap { if _, ok := r.componentGlobals[comp]; !ok { return fmt.Errorf("component not registered: %s", comp) } // only components without any dependencies can be set from the flag. if r.componentGlobals[comp].dependentEmulationVersion { return fmt.Errorf("EmulationVersion of %s is set by mapping, cannot set it by flag", comp) } } if emulationVersions, err := r.getFullEmulationVersionConfig(emulationVersionConfigMap); err != nil { return err } else { for comp, ver := range emulationVersions { r.componentGlobals[comp].effectiveVersion.SetEmulationVersion(ver) } } // Set feature gate emulation version before setting feature gate flag values. for comp, globals := range r.componentGlobals { if globals.featureGate == nil { continue } klog.V(klogLevel).Infof("setting %s:feature gate emulation version to %s", comp, globals.effectiveVersion.EmulationVersion().String()) if err := globals.featureGate.SetEmulationVersion(globals.effectiveVersion.EmulationVersion()); err != nil { return err } } for comp, fg := range r.featureGatesConfig { if comp == "" { if _, ok := r.featureGatesConfig[DefaultKubeComponent]; ok { return fmt.Errorf("set kube feature gates with default empty prefix or kube: prefix consistently, do not mix use") } comp = DefaultKubeComponent } if _, ok := r.componentGlobals[comp]; !ok { return fmt.Errorf("component not registered: %s", comp) } featureGate := r.componentGlobals[comp].featureGate if featureGate == nil { return fmt.Errorf("component featureGate not registered: %s", comp) } flagVal := strings.Join(fg, ",") klog.V(klogLevel).Infof("setting %s:feature-gates=%s", comp, flagVal) if err := featureGate.Set(flagVal); err != nil { return err } } return nil } func (r *componentGlobalsRegistry) Validate() []error { var errs []error r.mutex.Lock() defer r.mutex.Unlock() for _, globals := range r.componentGlobals { errs = append(errs, globals.effectiveVersion.Validate()...) if globals.featureGate != nil { errs = append(errs, globals.featureGate.Validate()...) } } return errs } func (r *componentGlobalsRegistry) SetEmulationVersionMapping(fromComponent, toComponent string, f VersionMapping) error { if f == nil { return nil } klog.V(klogLevel).Infof("setting EmulationVersion mapping from %s to %s", fromComponent, toComponent) r.mutex.Lock() defer r.mutex.Unlock() if _, ok := r.componentGlobals[fromComponent]; !ok { return fmt.Errorf("component not registered: %s", fromComponent) } if _, ok := r.componentGlobals[toComponent]; !ok { return fmt.Errorf("component not registered: %s", toComponent) } // check multiple dependency if r.componentGlobals[toComponent].dependentEmulationVersion { return fmt.Errorf("mapping of %s already exists from another component", toComponent) } r.componentGlobals[toComponent].dependentEmulationVersion = true versionMapping := r.componentGlobals[fromComponent].emulationVersionMapping if _, ok := versionMapping[toComponent]; ok { return fmt.Errorf("EmulationVersion from %s to %s already exists", fromComponent, toComponent) } versionMapping[toComponent] = f klog.V(klogLevel).Infof("setting the default EmulationVersion of %s based on mapping from the default EmulationVersion of %s", fromComponent, toComponent) defaultFromVersion := r.componentGlobals[fromComponent].effectiveVersion.EmulationVersion() emulationVersions, err := r.getFullEmulationVersionConfig(map[string]*version.Version{fromComponent: defaultFromVersion}) if err != nil { return err } for comp, ver := range emulationVersions { r.componentGlobals[comp].effectiveVersion.SetEmulationVersion(ver) } return nil } kubernetes-component-base-1b2882b/featuregate/registry_test.go000066400000000000000000000362261476422213000246670ustar00rootroot00000000000000/* 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 featuregate import ( "fmt" "strings" "testing" "github.com/spf13/pflag" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/version" baseversion "k8s.io/component-base/version" ) const ( testComponent = "test" ) func TestEffectiveVersionRegistry(t *testing.T) { r := NewComponentGlobalsRegistry() ver1 := baseversion.NewEffectiveVersion("1.31") ver2 := baseversion.NewEffectiveVersion("1.28") if r.EffectiveVersionFor(testComponent) != nil { t.Fatalf("expected nil EffectiveVersion initially") } if err := r.Register(testComponent, ver1, nil); err != nil { t.Fatalf("expected no error to register new component, but got err: %v", err) } if !r.EffectiveVersionFor(testComponent).EqualTo(ver1) { t.Fatalf("expected EffectiveVersionFor to return the version registered") } // overwrite if err := r.Register(testComponent, ver2, nil); err == nil { t.Fatalf("expected error to register existing component when override is false") } if !r.EffectiveVersionFor(testComponent).EqualTo(ver1) { t.Fatalf("expected EffectiveVersionFor to return the version overridden") } } func testRegistry(t *testing.T) *componentGlobalsRegistry { r := NewComponentGlobalsRegistry() verKube := baseversion.NewEffectiveVersion("1.31") fgKube := NewVersionedFeatureGate(version.MustParse("0.0")) err := fgKube.AddVersioned(map[Feature]VersionedSpecs{ "kubeA": { {Version: version.MustParse("1.31"), Default: true, LockToDefault: true, PreRelease: GA}, {Version: version.MustParse("1.28"), Default: false, PreRelease: Beta}, {Version: version.MustParse("1.27"), Default: false, PreRelease: Alpha}, }, "kubeB": { {Version: version.MustParse("1.30"), Default: false, PreRelease: Alpha}, }, "commonC": { {Version: version.MustParse("1.29"), Default: true, PreRelease: Beta}, {Version: version.MustParse("1.27"), Default: false, PreRelease: Alpha}, }, }) if err != nil { t.Fatal(err) } verTest := baseversion.NewEffectiveVersion("2.8") fgTest := NewVersionedFeatureGate(version.MustParse("0.0")) err = fgTest.AddVersioned(map[Feature]VersionedSpecs{ "testA": { {Version: version.MustParse("2.10"), Default: true, PreRelease: GA}, {Version: version.MustParse("2.8"), Default: false, PreRelease: Beta}, {Version: version.MustParse("2.7"), Default: false, PreRelease: Alpha}, }, "testB": { {Version: version.MustParse("2.9"), Default: false, PreRelease: Alpha}, }, "commonC": { {Version: version.MustParse("2.9"), Default: true, PreRelease: Beta}, {Version: version.MustParse("2.7"), Default: false, PreRelease: Alpha}, }, }) if err != nil { t.Fatal(err) } utilruntime.Must(r.Register(DefaultKubeComponent, verKube, fgKube)) utilruntime.Must(r.Register(testComponent, verTest, fgTest)) return r } func TestVersionFlagOptions(t *testing.T) { r := testRegistry(t) emuVers := strings.Join(r.unsafeVersionFlagOptions(true), "\n") expectedEmuVers := "kube=1.31..1.31 (default=1.31)\ntest=2.8..2.8 (default=2.8)" if emuVers != expectedEmuVers { t.Errorf("wanted emulation version flag options to be: %s, got %s", expectedEmuVers, emuVers) } minCompVers := strings.Join(r.unsafeVersionFlagOptions(false), "\n") expectedMinCompVers := "kube=1.30..1.31 (default=1.30)\ntest=2.7..2.8 (default=2.7)" if minCompVers != expectedMinCompVers { t.Errorf("wanted min compatibility version flag options to be: %s, got %s", expectedMinCompVers, minCompVers) } } func TestVersionFlagOptionsWithMapping(t *testing.T) { r := testRegistry(t) utilruntime.Must(r.SetEmulationVersionMapping(testComponent, DefaultKubeComponent, func(from *version.Version) *version.Version { return from.OffsetMinor(3) })) emuVers := strings.Join(r.unsafeVersionFlagOptions(true), "\n") expectedEmuVers := "test=2.8..2.8 (default=2.8)" if emuVers != expectedEmuVers { t.Errorf("wanted emulation version flag options to be: %s, got %s", expectedEmuVers, emuVers) } minCompVers := strings.Join(r.unsafeVersionFlagOptions(false), "\n") expectedMinCompVers := "kube=1.30..1.31 (default=1.30)\ntest=2.7..2.8 (default=2.7)" if minCompVers != expectedMinCompVers { t.Errorf("wanted min compatibility version flag options to be: %s, got %s", expectedMinCompVers, minCompVers) } } func TestVersionedFeatureGateFlags(t *testing.T) { r := testRegistry(t) known := strings.Join(r.unsafeKnownFeatures(), "\n") expectedKnown := "kube:AllAlpha=true|false (ALPHA - default=false)\n" + "kube:AllBeta=true|false (BETA - default=false)\n" + "kube:commonC=true|false (BETA - default=true)\n" + "kube:kubeB=true|false (ALPHA - default=false)\n" + "test:AllAlpha=true|false (ALPHA - default=false)\n" + "test:AllBeta=true|false (BETA - default=false)\n" + "test:commonC=true|false (ALPHA - default=false)\n" + "test:testA=true|false (BETA - default=false)" if known != expectedKnown { t.Errorf("wanted min compatibility version flag options to be:\n%s, got:\n%s", expectedKnown, known) } } func TestFlags(t *testing.T) { tests := []struct { name string flags []string parseError string expectedKubeEmulationVersion string expectedTestEmulationVersion string expectedKubeFeatureValues map[Feature]bool expectedTestFeatureValues map[Feature]bool }{ { name: "setting kube emulation version", flags: []string{"--emulated-version=kube=1.30"}, expectedKubeEmulationVersion: "1.30", }, { name: "setting kube emulation version twice", flags: []string{ "--emulated-version=kube=1.30", "--emulated-version=kube=1.32", }, parseError: "duplicate version flag, kube=1.30 and kube=1.32", }, { name: "prefix v ok", flags: []string{"--emulated-version=kube=v1.30"}, expectedKubeEmulationVersion: "1.30", }, { name: "patch version not ok", flags: []string{"--emulated-version=kube=1.30.2"}, parseError: "patch version not allowed, got: kube=1.30.2", }, { name: "setting test emulation version", flags: []string{"--emulated-version=test=2.7"}, expectedKubeEmulationVersion: "1.31", expectedTestEmulationVersion: "2.7", }, { name: "version missing component default to kube", flags: []string{"--emulated-version=1.30"}, expectedKubeEmulationVersion: "1.30", }, { name: "version missing component default to kube with duplicate", flags: []string{"--emulated-version=1.30", "--emulated-version=kube=1.30"}, parseError: "duplicate version flag, kube=1.30 and kube=1.30", }, { name: "version unregistered component", flags: []string{"--emulated-version=test3=1.31"}, parseError: "component not registered: test3", }, { name: "invalid version", flags: []string{"--emulated-version=test=1.foo"}, parseError: "illegal version string \"1.foo\"", }, { name: "setting test feature flag", flags: []string{ "--emulated-version=test=2.7", "--feature-gates=test:testA=true", }, expectedKubeEmulationVersion: "1.31", expectedTestEmulationVersion: "2.7", expectedKubeFeatureValues: map[Feature]bool{"kubeA": true, "kubeB": false, "commonC": true}, expectedTestFeatureValues: map[Feature]bool{"testA": true, "testB": false, "commonC": false}, }, { name: "setting future test feature flag", flags: []string{ "--emulated-version=test=2.7", "--feature-gates=test:testA=true,test:testB=true", }, parseError: "cannot set feature gate testB to true, feature is PreAlpha at emulated version 2.7", }, { name: "setting kube feature flag", flags: []string{ "--emulated-version=test=2.7", "--emulated-version=kube=1.30", "--feature-gates=kubeB=false,test:commonC=true", "--feature-gates=commonC=false,kubeB=true", }, expectedKubeEmulationVersion: "1.30", expectedTestEmulationVersion: "2.7", expectedKubeFeatureValues: map[Feature]bool{"kubeA": false, "kubeB": true, "commonC": false}, expectedTestFeatureValues: map[Feature]bool{"testA": false, "testB": false, "commonC": true}, }, { name: "setting kube feature flag with different prefix", flags: []string{ "--emulated-version=test=2.7", "--emulated-version=kube=1.30", "--feature-gates=kube:kubeB=false,test:commonC=true", "--feature-gates=commonC=false,kubeB=true", }, parseError: "set kube feature gates with default empty prefix or kube: prefix consistently, do not mix use", }, { name: "setting locked kube feature flag", flags: []string{ "--emulated-version=test=2.7", "--feature-gates=kubeA=false", }, parseError: "cannot set feature gate kubeA to false, feature is locked to true", }, { name: "setting unknown test feature flag", flags: []string{ "--emulated-version=test=2.7", "--feature-gates=test:testD=true", }, parseError: "unrecognized feature gate: testD", }, { name: "setting unknown component feature flag", flags: []string{ "--emulated-version=test=2.7", "--feature-gates=test3:commonC=true", }, parseError: "component not registered: test3", }, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { fs := pflag.NewFlagSet("testflag", pflag.ContinueOnError) r := testRegistry(t) r.AddFlags(fs) err := fs.Parse(test.flags) if err == nil { err = r.Set() } if test.parseError != "" { if err == nil || !strings.Contains(err.Error(), test.parseError) { t.Fatalf("%d: Parse() expected: %v, got: %v", i, test.parseError, err) } return } if err != nil { t.Fatalf("%d: Parse() expected: nil, got: %v", i, err) } if len(test.expectedKubeEmulationVersion) > 0 { assertVersionEqualTo(t, r.EffectiveVersionFor(DefaultKubeComponent).EmulationVersion(), test.expectedKubeEmulationVersion) } if len(test.expectedTestEmulationVersion) > 0 { assertVersionEqualTo(t, r.EffectiveVersionFor(testComponent).EmulationVersion(), test.expectedTestEmulationVersion) } for f, v := range test.expectedKubeFeatureValues { if r.FeatureGateFor(DefaultKubeComponent).Enabled(f) != v { t.Errorf("%d: expected kube feature Enabled(%s)=%v", i, f, v) } } for f, v := range test.expectedTestFeatureValues { if r.FeatureGateFor(testComponent).Enabled(f) != v { t.Errorf("%d: expected test feature Enabled(%s)=%v", i, f, v) } } }) } } func TestVersionMapping(t *testing.T) { r := NewComponentGlobalsRegistry() ver1 := baseversion.NewEffectiveVersion("0.58") ver2 := baseversion.NewEffectiveVersion("1.28") ver3 := baseversion.NewEffectiveVersion("2.10") utilruntime.Must(r.Register("test1", ver1, nil)) utilruntime.Must(r.Register("test2", ver2, nil)) utilruntime.Must(r.Register("test3", ver3, nil)) assertVersionEqualTo(t, r.EffectiveVersionFor("test1").EmulationVersion(), "0.58") assertVersionEqualTo(t, r.EffectiveVersionFor("test2").EmulationVersion(), "1.28") assertVersionEqualTo(t, r.EffectiveVersionFor("test3").EmulationVersion(), "2.10") utilruntime.Must(r.SetEmulationVersionMapping("test2", "test3", func(from *version.Version) *version.Version { return version.MajorMinor(from.Major()+1, from.Minor()-19) })) utilruntime.Must(r.SetEmulationVersionMapping("test1", "test2", func(from *version.Version) *version.Version { return version.MajorMinor(from.Major()+1, from.Minor()-28) })) assertVersionEqualTo(t, r.EffectiveVersionFor("test1").EmulationVersion(), "0.58") assertVersionEqualTo(t, r.EffectiveVersionFor("test2").EmulationVersion(), "1.30") assertVersionEqualTo(t, r.EffectiveVersionFor("test3").EmulationVersion(), "2.11") fs := pflag.NewFlagSet("testflag", pflag.ContinueOnError) r.AddFlags(fs) if err := fs.Parse([]string{fmt.Sprintf("--emulated-version=%s", "test1=0.56")}); err != nil { t.Fatal(err) return } if err := r.Set(); err != nil { t.Fatal(err) return } assertVersionEqualTo(t, r.EffectiveVersionFor("test1").EmulationVersion(), "0.56") assertVersionEqualTo(t, r.EffectiveVersionFor("test2").EmulationVersion(), "1.28") assertVersionEqualTo(t, r.EffectiveVersionFor("test3").EmulationVersion(), "2.09") } func TestVersionMappingWithMultipleDependency(t *testing.T) { r := NewComponentGlobalsRegistry() ver1 := baseversion.NewEffectiveVersion("0.58") ver2 := baseversion.NewEffectiveVersion("1.28") ver3 := baseversion.NewEffectiveVersion("2.10") utilruntime.Must(r.Register("test1", ver1, nil)) utilruntime.Must(r.Register("test2", ver2, nil)) utilruntime.Must(r.Register("test3", ver3, nil)) assertVersionEqualTo(t, r.EffectiveVersionFor("test1").EmulationVersion(), "0.58") assertVersionEqualTo(t, r.EffectiveVersionFor("test2").EmulationVersion(), "1.28") assertVersionEqualTo(t, r.EffectiveVersionFor("test3").EmulationVersion(), "2.10") utilruntime.Must(r.SetEmulationVersionMapping("test1", "test2", func(from *version.Version) *version.Version { return version.MajorMinor(from.Major()+1, from.Minor()-28) })) err := r.SetEmulationVersionMapping("test3", "test2", func(from *version.Version) *version.Version { return version.MajorMinor(from.Major()-1, from.Minor()+19) }) if err == nil { t.Errorf("expect error when setting 2nd mapping to test2") } } func TestVersionMappingWithCyclicDependency(t *testing.T) { r := NewComponentGlobalsRegistry() ver1 := baseversion.NewEffectiveVersion("0.58") ver2 := baseversion.NewEffectiveVersion("1.28") ver3 := baseversion.NewEffectiveVersion("2.10") utilruntime.Must(r.Register("test1", ver1, nil)) utilruntime.Must(r.Register("test2", ver2, nil)) utilruntime.Must(r.Register("test3", ver3, nil)) assertVersionEqualTo(t, r.EffectiveVersionFor("test1").EmulationVersion(), "0.58") assertVersionEqualTo(t, r.EffectiveVersionFor("test2").EmulationVersion(), "1.28") assertVersionEqualTo(t, r.EffectiveVersionFor("test3").EmulationVersion(), "2.10") utilruntime.Must(r.SetEmulationVersionMapping("test1", "test2", func(from *version.Version) *version.Version { return version.MajorMinor(from.Major()+1, from.Minor()-28) })) utilruntime.Must(r.SetEmulationVersionMapping("test2", "test3", func(from *version.Version) *version.Version { return version.MajorMinor(from.Major()+1, from.Minor()-19) })) err := r.SetEmulationVersionMapping("test3", "test1", func(from *version.Version) *version.Version { return version.MajorMinor(from.Major()-2, from.Minor()+48) }) if err == nil { t.Errorf("expect cyclic version mapping error") } } func assertVersionEqualTo(t *testing.T, ver *version.Version, expectedVer string) { if ver.EqualTo(version.MustParse(expectedVer)) { return } t.Errorf("expected: %s, got %s", expectedVer, ver.String()) } kubernetes-component-base-1b2882b/featuregate/testing/000077500000000000000000000000001476422213000230755ustar00rootroot00000000000000kubernetes-component-base-1b2882b/featuregate/testing/feature_gate.go000066400000000000000000000152101476422213000260560ustar00rootroot00000000000000/* Copyright 2017 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 testing import ( "fmt" "strings" "sync" "k8s.io/apimachinery/pkg/util/version" "k8s.io/component-base/featuregate" ) var ( overrideLock sync.Mutex featureFlagOverride map[featuregate.Feature]string emulationVersionOverride string emulationVersionOverrideValue *version.Version ) func init() { featureFlagOverride = map[featuregate.Feature]string{} } // SetFeatureGateDuringTest sets the specified gate to the specified value for duration of the test. // Fails when it detects second call to the same flag or is unable to set or restore feature flag. // // WARNING: Can leak set variable when called in test calling t.Parallel(), however second attempt to set the same feature flag will cause fatal. // // Example use: // // featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features., true) func SetFeatureGateDuringTest(tb TB, gate featuregate.FeatureGate, f featuregate.Feature, value bool) { tb.Helper() detectParallelOverrideCleanup := detectParallelOverride(tb, f) originalValue := gate.Enabled(f) originalEmuVer := gate.(featuregate.MutableVersionedFeatureGate).EmulationVersion() originalExplicitlySet := gate.(featuregate.MutableVersionedFeatureGate).ExplicitlySet(f) // Specially handle AllAlpha and AllBeta if f == "AllAlpha" || f == "AllBeta" { // Iterate over individual gates so their individual values get restored for k, v := range gate.(featuregate.MutableFeatureGate).GetAll() { if k == "AllAlpha" || k == "AllBeta" { continue } if (f == "AllAlpha" && v.PreRelease == featuregate.Alpha) || (f == "AllBeta" && v.PreRelease == featuregate.Beta) { SetFeatureGateDuringTest(tb, gate, k, value) } } } if err := gate.(featuregate.MutableFeatureGate).Set(fmt.Sprintf("%s=%v", f, value)); err != nil { tb.Errorf("error setting %s=%v: %v", f, value, err) } tb.Cleanup(func() { tb.Helper() detectParallelOverrideCleanup() emuVer := gate.(featuregate.MutableVersionedFeatureGate).EmulationVersion() if !emuVer.EqualTo(originalEmuVer) { tb.Fatalf("change of feature gate emulation version from %s to %s in the chain of SetFeatureGateDuringTest is not allowed\nuse SetFeatureGateEmulationVersionDuringTest to change emulation version in tests", originalEmuVer.String(), emuVer.String()) } if originalExplicitlySet { if err := gate.(featuregate.MutableFeatureGate).Set(fmt.Sprintf("%s=%v", f, originalValue)); err != nil { tb.Errorf("error restoring %s=%v: %v", f, originalValue, err) } } else { if err := gate.(featuregate.MutableVersionedFeatureGate).ResetFeatureValueToDefault(f); err != nil { tb.Errorf("error restoring %s=%v: %v", f, originalValue, err) } } }) } // SetFeatureGateEmulationVersionDuringTest sets the specified gate to the specified emulation version for duration of the test. // Fails when it detects second call to set a different emulation version or is unable to set or restore emulation version. // WARNING: Can leak set variable when called in test calling t.Parallel(), however second attempt to set a different emulation version will cause fatal. // Example use: // featuregatetesting.SetFeatureGateEmulationVersionDuringTest(t, utilfeature.DefaultFeatureGate, version.MustParse("1.31")) func SetFeatureGateEmulationVersionDuringTest(tb TB, gate featuregate.FeatureGate, ver *version.Version) { tb.Helper() detectParallelOverrideCleanup := detectParallelOverrideEmulationVersion(tb, ver) originalEmuVer := gate.(featuregate.MutableVersionedFeatureGate).EmulationVersion() if err := gate.(featuregate.MutableVersionedFeatureGate).SetEmulationVersion(ver); err != nil { tb.Fatalf("failed to set emulation version to %s during test: %v", ver.String(), err) } tb.Cleanup(func() { tb.Helper() detectParallelOverrideCleanup() if err := gate.(featuregate.MutableVersionedFeatureGate).SetEmulationVersion(originalEmuVer); err != nil { tb.Fatalf("failed to restore emulation version to %s during test", originalEmuVer.String()) } }) } func detectParallelOverride(tb TB, f featuregate.Feature) func() { tb.Helper() overrideLock.Lock() defer overrideLock.Unlock() beforeOverrideTestName := featureFlagOverride[f] if beforeOverrideTestName != "" && !sameTestOrSubtest(tb, beforeOverrideTestName) { tb.Fatalf("Detected parallel setting of a feature gate by both %q and %q", beforeOverrideTestName, tb.Name()) } featureFlagOverride[f] = tb.Name() return func() { tb.Helper() overrideLock.Lock() defer overrideLock.Unlock() if afterOverrideTestName := featureFlagOverride[f]; afterOverrideTestName != tb.Name() { tb.Fatalf("Detected parallel setting of a feature gate between both %q and %q", afterOverrideTestName, tb.Name()) } featureFlagOverride[f] = beforeOverrideTestName } } func detectParallelOverrideEmulationVersion(tb TB, ver *version.Version) func() { tb.Helper() overrideLock.Lock() defer overrideLock.Unlock() beforeOverrideTestName := emulationVersionOverride beforeOverrideValue := emulationVersionOverrideValue if ver.EqualTo(beforeOverrideValue) { return func() {} } if beforeOverrideTestName != "" && !sameTestOrSubtest(tb, beforeOverrideTestName) { tb.Fatalf("Detected parallel setting of a feature gate emulation version by both %q and %q", beforeOverrideTestName, tb.Name()) } emulationVersionOverride = tb.Name() emulationVersionOverrideValue = ver return func() { tb.Helper() overrideLock.Lock() defer overrideLock.Unlock() if afterOverrideTestName := emulationVersionOverride; afterOverrideTestName != tb.Name() { tb.Fatalf("Detected parallel setting of a feature gate emulation version between both %q and %q", afterOverrideTestName, tb.Name()) } emulationVersionOverride = beforeOverrideTestName emulationVersionOverrideValue = beforeOverrideValue } } func sameTestOrSubtest(tb TB, testName string) bool { // Assumes that "/" is not used in test names. return tb.Name() == testName || strings.HasPrefix(tb.Name(), testName+"/") } type TB interface { Cleanup(func()) Error(args ...any) Errorf(format string, args ...any) Fatal(args ...any) Fatalf(format string, args ...any) Helper() Name() string } kubernetes-component-base-1b2882b/featuregate/testing/feature_gate_test.go000066400000000000000000000347531476422213000271320ustar00rootroot00000000000000/* Copyright 2023 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 testing import ( gotest "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/version" "k8s.io/component-base/featuregate" ) func TestSpecialGates(t *gotest.T) { gate := featuregate.NewFeatureGate() gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{ "alpha_default_on": {PreRelease: featuregate.Alpha, Default: true}, "alpha_default_on_set_off": {PreRelease: featuregate.Alpha, Default: true}, "alpha_default_off": {PreRelease: featuregate.Alpha, Default: false}, "alpha_default_off_set_on": {PreRelease: featuregate.Alpha, Default: false}, "beta_default_on": {PreRelease: featuregate.Beta, Default: true}, "beta_default_on_set_off": {PreRelease: featuregate.Beta, Default: true}, "beta_default_off": {PreRelease: featuregate.Beta, Default: false}, "beta_default_off_set_on": {PreRelease: featuregate.Beta, Default: false}, "stable_default_on": {PreRelease: featuregate.GA, Default: true}, "stable_default_on_set_off": {PreRelease: featuregate.GA, Default: true}, "stable_default_off": {PreRelease: featuregate.GA, Default: false}, "stable_default_off_set_on": {PreRelease: featuregate.GA, Default: false}, }) gate.Set("alpha_default_on_set_off=false") gate.Set("beta_default_on_set_off=false") gate.Set("stable_default_on_set_off=false") gate.Set("alpha_default_off_set_on=true") gate.Set("beta_default_off_set_on=true") gate.Set("stable_default_off_set_on=true") before := map[featuregate.Feature]bool{ "AllAlpha": false, "AllBeta": false, "alpha_default_on": true, "alpha_default_on_set_off": false, "alpha_default_off": false, "alpha_default_off_set_on": true, "beta_default_on": true, "beta_default_on_set_off": false, "beta_default_off": false, "beta_default_off_set_on": true, "stable_default_on": true, "stable_default_on_set_off": false, "stable_default_off": false, "stable_default_off_set_on": true, } expect(t, gate, before) t.Cleanup(func() { expect(t, gate, before) cleanup() }) SetFeatureGateDuringTest(t, gate, "AllAlpha", true) expect(t, gate, map[featuregate.Feature]bool{ "AllAlpha": true, "AllBeta": false, "alpha_default_on": true, "alpha_default_on_set_off": true, "alpha_default_off": true, "alpha_default_off_set_on": true, "beta_default_on": true, "beta_default_on_set_off": false, "beta_default_off": false, "beta_default_off_set_on": true, "stable_default_on": true, "stable_default_on_set_off": false, "stable_default_off": false, "stable_default_off_set_on": true, }) SetFeatureGateDuringTest(t, gate, "AllBeta", true) expect(t, gate, map[featuregate.Feature]bool{ "AllAlpha": true, "AllBeta": true, "alpha_default_on": true, "alpha_default_on_set_off": true, "alpha_default_off": true, "alpha_default_off_set_on": true, "beta_default_on": true, "beta_default_on_set_off": true, "beta_default_off": true, "beta_default_off_set_on": true, "stable_default_on": true, "stable_default_on_set_off": false, "stable_default_off": false, "stable_default_off_set_on": true, }) } func expect(t *gotest.T, gate featuregate.FeatureGate, expect map[featuregate.Feature]bool) { t.Helper() for k, v := range expect { if gate.Enabled(k) != v { t.Errorf("Expected %v=%v, got %v", k, v, gate.Enabled(k)) } } } func TestSetFeatureGateInTest(t *gotest.T) { gate := featuregate.NewFeatureGate() err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{ "feature": {PreRelease: featuregate.Alpha, Default: false}, }) require.NoError(t, err) assert.False(t, gate.Enabled("feature")) SetFeatureGateDuringTest(t, gate, "feature", true) SetFeatureGateDuringTest(t, gate, "feature", true) assert.True(t, gate.Enabled("feature")) t.Run("Subtest", func(t *gotest.T) { assert.True(t, gate.Enabled("feature")) }) t.Run("ParallelSubtest", func(t *gotest.T) { assert.True(t, gate.Enabled("feature")) // Calling t.Parallel in subtest will resume the main test body t.Parallel() assert.True(t, gate.Enabled("feature")) }) assert.True(t, gate.Enabled("feature")) t.Run("OverwriteInSubtest", func(t *gotest.T) { SetFeatureGateDuringTest(t, gate, "feature", false) assert.False(t, gate.Enabled("feature")) }) assert.True(t, gate.Enabled("feature")) } func TestSpecialGatesVersioned(t *gotest.T) { originalEmulationVersion := version.MustParse("1.31") gate := featuregate.NewVersionedFeatureGate(originalEmulationVersion) err := gate.AddVersioned(map[featuregate.Feature]featuregate.VersionedSpecs{ "alpha_default_on": { {Version: version.MustParse("1.27"), Default: true, PreRelease: featuregate.Alpha}, }, "alpha_default_off": { {Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Alpha}, }, "beta_default_on": { {Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, }, "beta_default_on_set_off": { {Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, {Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Alpha}, }, "beta_default_off": { {Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Beta}, }, "beta_default_off_set_on": { {Version: version.MustParse("1.31"), Default: false, PreRelease: featuregate.Beta}, {Version: version.MustParse("1.27"), Default: false, PreRelease: featuregate.Alpha}, }, }) require.NoError(t, err) require.NoError(t, gate.Set("beta_default_on_set_off=false")) require.NoError(t, gate.Set("beta_default_off_set_on=true")) before := map[featuregate.Feature]bool{ "AllAlpha": false, "AllBeta": false, "alpha_default_on": true, "alpha_default_off": false, "beta_default_on": true, "beta_default_on_set_off": false, "beta_default_off": false, "beta_default_off_set_on": true, } expect(t, gate, before) t.Cleanup(func() { expect(t, gate, before) cleanup() }) t.Run("OverwriteInSubtest", func(t *gotest.T) { SetFeatureGateDuringTest(t, gate, "AllAlpha", true) expect(t, gate, map[featuregate.Feature]bool{ "AllAlpha": true, "AllBeta": false, "alpha_default_on": true, "alpha_default_off": true, "beta_default_on": true, "beta_default_on_set_off": false, "beta_default_off": false, "beta_default_off_set_on": true, }) SetFeatureGateDuringTest(t, gate, "AllBeta", true) expect(t, gate, map[featuregate.Feature]bool{ "AllAlpha": true, "AllBeta": true, "alpha_default_on": true, "alpha_default_off": true, "beta_default_on": true, "beta_default_on_set_off": true, "beta_default_off": true, "beta_default_off_set_on": true, }) }) expect(t, gate, before) t.Run("OverwriteInSubtestAtDifferentVersion", func(t *gotest.T) { SetFeatureGateEmulationVersionDuringTest(t, gate, version.MustParse("1.28")) SetFeatureGateDuringTest(t, gate, "AllAlpha", true) expect(t, gate, map[featuregate.Feature]bool{ "AllAlpha": true, "AllBeta": false, "alpha_default_on": true, "alpha_default_off": true, "beta_default_on": false, "beta_default_on_set_off": true, "beta_default_off": false, "beta_default_off_set_on": true, }) }) } func TestDetectLeakToMainTest(t *gotest.T) { t.Cleanup(cleanup) gate := featuregate.NewFeatureGate() err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{ "feature": {PreRelease: featuregate.Alpha, Default: false}, }) require.NoError(t, err) // Subtest setting feature gate and calling parallel will leak it out t.Run("LeakingSubtest", func(t *gotest.T) { fakeT := &ignoreFatalT{T: t} SetFeatureGateDuringTest(fakeT, gate, "feature", true) // Calling t.Parallel in subtest will resume the main test body t.Parallel() // Leaked false from main test assert.False(t, gate.Enabled("feature")) }) // Leaked true from subtest assert.True(t, gate.Enabled("feature")) fakeT := &ignoreFatalT{T: t} SetFeatureGateDuringTest(fakeT, gate, "feature", false) assert.True(t, fakeT.fatalRecorded) } func TestDetectLeakToOtherSubtest(t *gotest.T) { t.Cleanup(cleanup) gate := featuregate.NewFeatureGate() err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{ "feature": {PreRelease: featuregate.Alpha, Default: false}, }) require.NoError(t, err) subtestName := "Subtest" // Subtest setting feature gate and calling parallel will leak it out t.Run(subtestName, func(t *gotest.T) { fakeT := &ignoreFatalT{T: t} SetFeatureGateDuringTest(fakeT, gate, "feature", true) t.Parallel() }) // Add suffix to name to prevent tests with the same prefix. t.Run(subtestName+"Suffix", func(t *gotest.T) { // Leaked true assert.True(t, gate.Enabled("feature")) fakeT := &ignoreFatalT{T: t} SetFeatureGateDuringTest(fakeT, gate, "feature", false) assert.True(t, fakeT.fatalRecorded) }) } func TestCannotDetectLeakFromSubtest(t *gotest.T) { t.Cleanup(cleanup) gate := featuregate.NewFeatureGate() err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{ "feature": {PreRelease: featuregate.Alpha, Default: false}, }) require.NoError(t, err) SetFeatureGateDuringTest(t, gate, "feature", false) // Subtest setting feature gate and calling parallel will leak it out t.Run("Subtest", func(t *gotest.T) { SetFeatureGateDuringTest(t, gate, "feature", true) t.Parallel() }) // Leaked true assert.True(t, gate.Enabled("feature")) } func TestCannotDetectLeakFromTwoSubtestsWithDifferentFeatures(t *gotest.T) { t.Cleanup(cleanup) gate := featuregate.NewFeatureGate() err := gate.Add(map[featuregate.Feature]featuregate.FeatureSpec{ "feature1": {PreRelease: featuregate.Alpha, Default: false}, "feature2": {PreRelease: featuregate.Alpha, Default: false}, }) require.NoError(t, err) assert.False(t, gate.Enabled("feature1")) assert.False(t, gate.Enabled("feature2")) subtestName := "Subtest" // Subtest setting feature gate and calling parallel will leak it out t.Run(subtestName, func(t *gotest.T) { SetFeatureGateDuringTest(t, gate, "feature1", true) t.Parallel() assert.True(t, gate.Enabled("feature1")) assert.False(t, gate.Enabled("feature2")) }) // Leaked true assert.True(t, gate.Enabled("feature1")) assert.False(t, gate.Enabled("feature2")) // Add suffix to name to prevent tests with the same prefix. t.Run(subtestName+"Suffix", func(t *gotest.T) { // Leaked true assert.True(t, gate.Enabled("feature1")) assert.False(t, gate.Enabled("feature2")) SetFeatureGateDuringTest(t, gate, "feature2", true) assert.True(t, gate.Enabled("feature1")) assert.True(t, gate.Enabled("feature2")) }) } func TestDetectEmulationVersionLeakToMainTest(t *gotest.T) { t.Cleanup(cleanup) originalEmulationVersion := version.MustParse("1.31") newEmulationVersion := version.MustParse("1.30") gate := featuregate.NewVersionedFeatureGate(originalEmulationVersion) assert.True(t, gate.EmulationVersion().EqualTo(originalEmulationVersion)) // Subtest setting feature gate and calling parallel will leak it out t.Run("LeakingSubtest", func(t *gotest.T) { fakeT := &ignoreFatalT{T: t} SetFeatureGateEmulationVersionDuringTest(fakeT, gate, newEmulationVersion) // Calling t.Parallel in subtest will resume the main test body t.Parallel() // Leaked from main test assert.True(t, gate.EmulationVersion().EqualTo(originalEmulationVersion)) }) // Leaked from subtest assert.True(t, gate.EmulationVersion().EqualTo(newEmulationVersion)) fakeT := &ignoreFatalT{T: t} SetFeatureGateEmulationVersionDuringTest(fakeT, gate, originalEmulationVersion) assert.True(t, fakeT.fatalRecorded) } func TestNoLeakFromSameEmulationVersionToMainTest(t *gotest.T) { t.Cleanup(cleanup) originalEmulationVersion := version.MustParse("1.31") newEmulationVersion := version.MustParse("1.31") gate := featuregate.NewVersionedFeatureGate(originalEmulationVersion) assert.True(t, gate.EmulationVersion().EqualTo(originalEmulationVersion)) // Subtest setting feature gate and calling parallel will leak it out t.Run("LeakingSubtest", func(t *gotest.T) { SetFeatureGateEmulationVersionDuringTest(t, gate, newEmulationVersion) // Calling t.Parallel in subtest will resume the main test body t.Parallel() // Leaked from main test assert.True(t, gate.EmulationVersion().EqualTo(originalEmulationVersion)) }) // Leaked from subtest assert.True(t, gate.EmulationVersion().EqualTo(newEmulationVersion)) SetFeatureGateEmulationVersionDuringTest(t, gate, originalEmulationVersion) } func TestDetectEmulationVersionLeakToOtherSubtest(t *gotest.T) { t.Cleanup(cleanup) originalEmulationVersion := version.MustParse("1.31") newEmulationVersion := version.MustParse("1.30") gate := featuregate.NewVersionedFeatureGate(originalEmulationVersion) assert.True(t, gate.EmulationVersion().EqualTo(originalEmulationVersion)) subtestName := "Subtest" // Subtest setting feature gate and calling parallel will leak it out t.Run(subtestName, func(t *gotest.T) { fakeT := &ignoreFatalT{T: t} SetFeatureGateEmulationVersionDuringTest(fakeT, gate, newEmulationVersion) t.Parallel() }) // Add suffix to name to prevent tests with the same prefix. t.Run(subtestName+"Suffix", func(t *gotest.T) { // Leaked newEmulationVersion assert.True(t, gate.EmulationVersion().EqualTo(newEmulationVersion)) fakeT := &ignoreFatalT{T: t} SetFeatureGateEmulationVersionDuringTest(fakeT, gate, originalEmulationVersion) assert.True(t, fakeT.fatalRecorded) }) } type ignoreFatalT struct { *gotest.T fatalRecorded bool } func (f *ignoreFatalT) Fatal(args ...any) { f.T.Helper() f.fatalRecorded = true newArgs := []any{"[IGNORED]"} newArgs = append(newArgs, args...) f.T.Log(newArgs...) } func (f *ignoreFatalT) Fatalf(format string, args ...any) { f.T.Helper() f.fatalRecorded = true f.T.Logf("[IGNORED] "+format, args...) } func cleanup() { featureFlagOverride = map[featuregate.Feature]string{} emulationVersionOverride = "" emulationVersionOverrideValue = nil } kubernetes-component-base-1b2882b/go.mod000066400000000000000000000067741476422213000202500ustar00rootroot00000000000000// This is a generated file. Do not edit directly. module k8s.io/component-base go 1.23.0 godebug default=go1.23 godebug winsymlink=0 require ( github.com/blang/semver/v4 v4.0.0 github.com/go-logr/logr v1.4.2 github.com/go-logr/zapr v1.3.0 github.com/google/go-cmp v0.6.0 github.com/moby/term v0.5.0 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.55.0 github.com/prometheus/procfs v0.15.1 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 go.opentelemetry.io/otel/sdk v1.28.0 go.opentelemetry.io/otel/trace v1.28.0 go.uber.org/zap v1.27.0 golang.org/x/sys v0.26.0 k8s.io/apimachinery v0.32.3 k8s.io/client-go v0.32.3 k8s.io/klog/v2 v2.130.1 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 sigs.k8s.io/yaml v1.4.0 ) require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/term v0.25.0 // indirect golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.7.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.32.3 // indirect k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect ) kubernetes-component-base-1b2882b/go.sum000066400000000000000000000473211476422213000202660ustar00rootroot00000000000000github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw= google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= kubernetes-component-base-1b2882b/logs/000077500000000000000000000000001476422213000200705ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/OWNERS000066400000000000000000000003551476422213000210330ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners approvers: - sig-instrumentation-approvers - serathius - pohly reviewers: - sig-instrumentation-reviewers - serathius labels: - sig/instrumentation - wg/structured-logging kubernetes-component-base-1b2882b/logs/api/000077500000000000000000000000001476422213000206415ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/api/OWNERS000066400000000000000000000002541476422213000216020ustar00rootroot00000000000000# Disable inheritance as this is an api owners file options: no_parent_owners: true approvers: - api-approvers reviewers: - api-reviewers labels: - kind/api-change kubernetes-component-base-1b2882b/logs/api/v1/000077500000000000000000000000001476422213000211675ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/api/v1/doc.go000066400000000000000000000025541476422213000222710ustar00rootroot00000000000000/* Copyright 2022 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. */ // +k8s:deepcopy-gen=package // Package v1 contains the configuration API for logging. // // The intention is to only have a single version of this API, potentially with // new fields added over time in a backwards-compatible manner. Fields for // alpha or beta features are allowed as long as they are defined so that not // changing the defaults leaves those features disabled. // // The "v1" package name is just a reminder that API compatibility rules apply, // not an indication of the stability of all features covered by it. // The LoggingAlphaOptions and LoggingBetaOptions feature gates control whether // these unstable features can get enabled. This can be used to ensure that // command invocations do not accidentally rely on unstable features. package v1 // import "k8s.io/component-base/logs/api/v1" kubernetes-component-base-1b2882b/logs/api/v1/features_test.go000066400000000000000000000025471476422213000244030ustar00rootroot00000000000000/* Copyright 2022 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 v1 import ( "k8s.io/component-base/featuregate" ) var ( // pre-defined feature gates with the features from this package in a // certain state (default, all enabled, all disabled). defaultFeatureGate, enabledFeatureGate, disabledFeatureGate featuregate.FeatureGate ) func init() { mutable := featuregate.NewFeatureGate() if err := AddFeatureGates(mutable); err != nil { panic(err) } defaultFeatureGate = mutable enabled := mutable.DeepCopy() disabled := mutable.DeepCopy() for feature := range mutable.GetAll() { if err := enabled.SetFromMap(map[string]bool{string(feature): true}); err != nil { panic(err) } if err := disabled.SetFromMap(map[string]bool{string(feature): false}); err != nil { panic(err) } } enabledFeatureGate = enabled disabledFeatureGate = disabled } kubernetes-component-base-1b2882b/logs/api/v1/kube_features.go000066400000000000000000000052121476422213000243420ustar00rootroot00000000000000/* Copyright 2022 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 v1 import ( "k8s.io/component-base/featuregate" ) const ( // owner: @pohly // kep: https://kep.k8s.io/3077 // alpha: v1.24 // beta: v1.30 // // Enables looking up a logger from a context.Context instead of using // the global fallback logger and manipulating the logger that is // used by a call chain. ContextualLogging featuregate.Feature = "ContextualLogging" // contextualLoggingDefault is now true because the feature reached beta // and performance comparisons showed no relevant degradation when // enabling it. contextualLoggingDefault = true // Allow fine-tuning of experimental, alpha-quality logging options. // // Per https://groups.google.com/g/kubernetes-sig-architecture/c/Nxsc7pfe5rw/m/vF2djJh0BAAJ // we want to avoid a proliferation of feature gates. This feature gate: // - will guard *a group* of logging options whose quality level is alpha. // - will never graduate to beta or stable. LoggingAlphaOptions featuregate.Feature = "LoggingAlphaOptions" // Allow fine-tuning of experimental, beta-quality logging options. // // Per https://groups.google.com/g/kubernetes-sig-architecture/c/Nxsc7pfe5rw/m/vF2djJh0BAAJ // we want to avoid a proliferation of feature gates. This feature gate: // - will guard *a group* of logging options whose quality level is beta. // - is thus *introduced* as beta // - will never graduate to stable. LoggingBetaOptions featuregate.Feature = "LoggingBetaOptions" // Stable logging options. Always enabled. LoggingStableOptions featuregate.Feature = "LoggingStableOptions" ) func featureGates() map[featuregate.Feature]featuregate.FeatureSpec { return map[featuregate.Feature]featuregate.FeatureSpec{ ContextualLogging: {Default: contextualLoggingDefault, PreRelease: featuregate.Beta}, LoggingAlphaOptions: {Default: false, PreRelease: featuregate.Alpha}, LoggingBetaOptions: {Default: true, PreRelease: featuregate.Beta}, } } // AddFeatureGates adds all feature gates used by this package. func AddFeatureGates(mutableFeatureGate featuregate.MutableFeatureGate) error { return mutableFeatureGate.Add(featureGates()) } kubernetes-component-base-1b2882b/logs/api/v1/options.go000066400000000000000000000431651476422213000232220ustar00rootroot00000000000000/* Copyright 2021 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 v1 import ( "errors" "flag" "fmt" "io" "math" "os" "strings" "sync/atomic" "time" "github.com/google/go-cmp/cmp" "github.com/spf13/pflag" "k8s.io/klog/v2" "k8s.io/klog/v2/textlogger" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/validation/field" cliflag "k8s.io/component-base/cli/flag" "k8s.io/component-base/featuregate" "k8s.io/component-base/logs/internal/setverbositylevel" "k8s.io/component-base/logs/klogflags" ) const ( // LogFlushFreqDefault is the default for the corresponding command line // parameter. LogFlushFreqDefault = 5 * time.Second ) const ( // LogFlushFreqFlagName is the name of the command line parameter. // Depending on how flags get added, it is either a stand-alone // value (logs.AddFlags) or part of LoggingConfiguration. LogFlushFreqFlagName = "log-flush-frequency" ) // NewLoggingConfiguration returns a struct holding the default logging configuration. func NewLoggingConfiguration() *LoggingConfiguration { c := LoggingConfiguration{} SetRecommendedLoggingConfiguration(&c) return &c } // Applying configurations multiple times is not safe unless it's guaranteed that there // are no goroutines which might call logging functions. The default for ValidateAndApply // and ValidateAndApplyWithOptions is to return an error when called more than once. // Binaries and unit tests can override that behavior. var ReapplyHandling = ReapplyHandlingError type ReapplyHandlingType int const ( // ReapplyHandlingError is the default: calling ValidateAndApply or // ValidateAndApplyWithOptions again returns an error. ReapplyHandlingError ReapplyHandlingType = iota // ReapplyHandlingIgnoreUnchanged silently ignores any additional calls of // ValidateAndApply or ValidateAndApplyWithOptions if the configuration // is unchanged, otherwise they return an error. ReapplyHandlingIgnoreUnchanged ) // ValidateAndApply combines validation and application of the logging configuration. // This should be invoked as early as possible because then the rest of the program // startup (including validation of other options) will already run with the final // logging configuration. // // The optional FeatureGate controls logging features. If nil, the default for // these features is used. // // Logging options must be applied as early as possible during the program // startup. Some changes are global and cannot be done safely when there are // already goroutines running. func ValidateAndApply(c *LoggingConfiguration, featureGate featuregate.FeatureGate) error { return validateAndApply(c, nil, featureGate, nil) } // ValidateAndApplyWithOptions is a variant of ValidateAndApply which accepts // additional options beyond those that can be configured through the API. This // is meant for testing. // // Logging options must be applied as early as possible during the program // startup. Some changes are global and cannot be done safely when there are // already goroutines running. func ValidateAndApplyWithOptions(c *LoggingConfiguration, options *LoggingOptions, featureGate featuregate.FeatureGate) error { return validateAndApply(c, options, featureGate, nil) } // +k8s:deepcopy-gen=false // LoggingOptions can be used with ValidateAndApplyWithOptions to override // certain global defaults. type LoggingOptions struct { // ErrorStream can be used to override the os.Stderr default. ErrorStream io.Writer // InfoStream can be used to override the os.Stdout default. InfoStream io.Writer } // ValidateAndApplyAsField is a variant of ValidateAndApply that should be used // when the LoggingConfiguration is embedded in some larger configuration // structure. func ValidateAndApplyAsField(c *LoggingConfiguration, featureGate featuregate.FeatureGate, fldPath *field.Path) error { return validateAndApply(c, nil, featureGate, fldPath) } func validateAndApply(c *LoggingConfiguration, options *LoggingOptions, featureGate featuregate.FeatureGate, fldPath *field.Path) error { errs := Validate(c, featureGate, fldPath) if len(errs) > 0 { return errs.ToAggregate() } return apply(c, options, featureGate) } // Validate can be used to check for invalid settings without applying them. // Most binaries should validate and apply the logging configuration as soon // as possible via ValidateAndApply. The field path is optional: nil // can be passed when the struct is not embedded in some larger struct. func Validate(c *LoggingConfiguration, featureGate featuregate.FeatureGate, fldPath *field.Path) field.ErrorList { errs := field.ErrorList{} if c.Format != DefaultLogFormat { // WordSepNormalizeFunc is just a guess. Commands should use it, // but we cannot know for sure. allFlags := unsupportedLoggingFlags(cliflag.WordSepNormalizeFunc) for _, f := range allFlags { if f.DefValue != f.Value.String() { errs = append(errs, field.Invalid(fldPath.Child("format"), c.Format, fmt.Sprintf("Non-default format doesn't honor flag: %s", f.Name))) } } } format, err := logRegistry.get(c.Format) if err != nil { errs = append(errs, field.Invalid(fldPath.Child("format"), c.Format, "Unsupported log format")) } else if format != nil { if format.feature != LoggingStableOptions { enabled := featureGates()[format.feature].Default if featureGate != nil { enabled = featureGate.Enabled(format.feature) } if !enabled { errs = append(errs, field.Forbidden(fldPath.Child("format"), fmt.Sprintf("Log format %s is disabled, see %s feature", c.Format, format.feature))) } } } // The type in our struct is uint32, but klog only accepts positive int32. if c.Verbosity > math.MaxInt32 { errs = append(errs, field.Invalid(fldPath.Child("verbosity"), c.Verbosity, fmt.Sprintf("Must be <= %d", math.MaxInt32))) } vmoduleFldPath := fldPath.Child("vmodule") if len(c.VModule) > 0 && c.Format != "" && c.Format != "text" { errs = append(errs, field.Forbidden(vmoduleFldPath, "Only supported for text log format")) } for i, item := range c.VModule { if item.FilePattern == "" { errs = append(errs, field.Required(vmoduleFldPath.Index(i), "File pattern must not be empty")) } if strings.ContainsAny(item.FilePattern, "=,") { errs = append(errs, field.Invalid(vmoduleFldPath.Index(i), item.FilePattern, "File pattern must not contain equal sign or comma")) } if item.Verbosity > math.MaxInt32 { errs = append(errs, field.Invalid(vmoduleFldPath.Index(i), item.Verbosity, fmt.Sprintf("Must be <= %d", math.MaxInt32))) } } errs = append(errs, validateFormatOptions(c, featureGate, fldPath.Child("options"))...) return errs } func validateFormatOptions(c *LoggingConfiguration, featureGate featuregate.FeatureGate, fldPath *field.Path) field.ErrorList { errs := field.ErrorList{} errs = append(errs, validateTextOptions(c, featureGate, fldPath.Child("text"))...) errs = append(errs, validateJSONOptions(c, featureGate, fldPath.Child("json"))...) return errs } func validateTextOptions(c *LoggingConfiguration, featureGate featuregate.FeatureGate, fldPath *field.Path) field.ErrorList { errs := field.ErrorList{} if gate := LoggingAlphaOptions; c.Options.Text.SplitStream && !featureEnabled(featureGate, gate) { errs = append(errs, field.Forbidden(fldPath.Child("splitStream"), fmt.Sprintf("Feature %s is disabled", gate))) } if gate := LoggingAlphaOptions; c.Options.Text.InfoBufferSize.Value() != 0 && !featureEnabled(featureGate, gate) { errs = append(errs, field.Forbidden(fldPath.Child("infoBufferSize"), fmt.Sprintf("Feature %s is disabled", gate))) } return errs } func validateJSONOptions(c *LoggingConfiguration, featureGate featuregate.FeatureGate, fldPath *field.Path) field.ErrorList { errs := field.ErrorList{} if gate := LoggingAlphaOptions; c.Options.JSON.SplitStream && !featureEnabled(featureGate, gate) { errs = append(errs, field.Forbidden(fldPath.Child("splitStream"), fmt.Sprintf("Feature %s is disabled", gate))) } if gate := LoggingAlphaOptions; c.Options.JSON.InfoBufferSize.Value() != 0 && !featureEnabled(featureGate, gate) { errs = append(errs, field.Forbidden(fldPath.Child("infoBufferSize"), fmt.Sprintf("Feature %s is disabled", gate))) } return errs } func featureEnabled(featureGate featuregate.FeatureGate, feature featuregate.Feature) bool { enabled := false if featureGate != nil { enabled = featureGate.Enabled(feature) } return enabled } func apply(c *LoggingConfiguration, options *LoggingOptions, featureGate featuregate.FeatureGate) error { p := ¶meters{ C: c, Options: options, ContextualLoggingEnabled: contextualLoggingDefault, } if featureGate != nil { p.ContextualLoggingEnabled = featureGate.Enabled(ContextualLogging) } oldP := applyParameters.Load() if oldP != nil { switch ReapplyHandling { case ReapplyHandlingError: return errors.New("logging configuration was already applied earlier, changing it is not allowed") case ReapplyHandlingIgnoreUnchanged: if diff := cmp.Diff(oldP, p); diff != "" { return fmt.Errorf("the logging configuration should not be changed after setting it once (- old setting, + new setting):\n%s", diff) } return nil default: return fmt.Errorf("invalid value %d for ReapplyHandling", ReapplyHandling) } } applyParameters.Store(p) // if log format not exists, use nil loggr format, _ := logRegistry.get(c.Format) if format.factory == nil { klog.ClearLogger() } else { if options == nil { options = &LoggingOptions{ ErrorStream: os.Stderr, InfoStream: os.Stdout, } } log, control := format.factory.Create(*c, *options) if control.SetVerbosityLevel != nil { setverbositylevel.Mutex.Lock() defer setverbositylevel.Mutex.Unlock() setverbositylevel.Callbacks = append(setverbositylevel.Callbacks, control.SetVerbosityLevel) } opts := []klog.LoggerOption{ klog.ContextualLogger(p.ContextualLoggingEnabled), klog.FlushLogger(control.Flush), } if writer, ok := log.GetSink().(textlogger.KlogBufferWriter); ok { opts = append(opts, klog.WriteKlogBuffer(writer.WriteKlogBuffer)) } klog.SetLoggerWithOptions(log, opts...) } if err := loggingFlags.Lookup("v").Value.Set(VerbosityLevelPflag(&c.Verbosity).String()); err != nil { return fmt.Errorf("internal error while setting klog verbosity: %v", err) } if err := loggingFlags.Lookup("vmodule").Value.Set(VModuleConfigurationPflag(&c.VModule).String()); err != nil { return fmt.Errorf("internal error while setting klog vmodule: %v", err) } setSlogDefaultLogger() klog.StartFlushDaemon(c.FlushFrequency.Duration.Duration) klog.EnableContextualLogging(p.ContextualLoggingEnabled) return nil } type parameters struct { C *LoggingConfiguration Options *LoggingOptions ContextualLoggingEnabled bool } var applyParameters atomic.Pointer[parameters] // ResetForTest restores the default settings. This is not thread-safe and should only // be used when there are no goroutines running. The intended users are unit // tests in other packages. func ResetForTest(featureGate featuregate.FeatureGate) error { oldP := applyParameters.Load() if oldP == nil { // Nothing to do. return nil } // This makes it possible to call apply again without triggering errors. applyParameters.Store(nil) // Restore defaults. Shouldn't fail, but check anyway. config := NewLoggingConfiguration() if err := ValidateAndApply(config, featureGate); err != nil { return fmt.Errorf("apply default configuration: %v", err) } // And again... applyParameters.Store(nil) return nil } // AddFlags adds command line flags for the configuration. func AddFlags(c *LoggingConfiguration, fs *pflag.FlagSet) { addFlags(c, fs) } // AddGoFlags is a variant of AddFlags for a standard FlagSet. func AddGoFlags(c *LoggingConfiguration, fs *flag.FlagSet) { addFlags(c, goFlagSet{FlagSet: fs}) } // flagSet is the interface implemented by pflag.FlagSet, with // just those methods defined which are needed by addFlags. type flagSet interface { BoolVar(p *bool, name string, value bool, usage string) DurationVar(p *time.Duration, name string, value time.Duration, usage string) StringVar(p *string, name string, value string, usage string) Var(value pflag.Value, name string, usage string) VarP(value pflag.Value, name, shorthand, usage string) } // goFlagSet implements flagSet for a stdlib flag.FlagSet. type goFlagSet struct { *flag.FlagSet } func (fs goFlagSet) Var(value pflag.Value, name string, usage string) { fs.FlagSet.Var(value, name, usage) } func (fs goFlagSet) VarP(value pflag.Value, name, shorthand, usage string) { // Ignore shorthand, it's not needed and not supported. fs.FlagSet.Var(value, name, usage) } // addFlags can be used with both flag.FlagSet and pflag.FlagSet. The internal // interface definition avoids duplicating this code. func addFlags(c *LoggingConfiguration, fs flagSet) { formats := logRegistry.list() fs.StringVar(&c.Format, "logging-format", c.Format, fmt.Sprintf("Sets the log format. Permitted formats: %s.", formats)) // No new log formats should be added after generation is of flag options logRegistry.freeze() fs.DurationVar(&c.FlushFrequency.Duration.Duration, LogFlushFreqFlagName, c.FlushFrequency.Duration.Duration, "Maximum number of seconds between log flushes") fs.VarP(VerbosityLevelPflag(&c.Verbosity), "v", "v", "number for the log level verbosity") fs.Var(VModuleConfigurationPflag(&c.VModule), "vmodule", "comma-separated list of pattern=N settings for file-filtered logging (only works for text log format)") fs.BoolVar(&c.Options.Text.SplitStream, "log-text-split-stream", false, "[Alpha] In text format, write error messages to stderr and info messages to stdout. The default is to write a single stream to stdout. Enable the LoggingAlphaOptions feature gate to use this.") fs.Var(&c.Options.Text.InfoBufferSize, "log-text-info-buffer-size", "[Alpha] In text format with split output streams, the info messages can be buffered for a while to increase performance. The default value of zero bytes disables buffering. The size can be specified as number of bytes (512), multiples of 1000 (1K), multiples of 1024 (2Ki), or powers of those (3M, 4G, 5Mi, 6Gi). Enable the LoggingAlphaOptions feature gate to use this.") // JSON options. We only register them if "json" is a valid format. The // config file API however always has them. if _, err := logRegistry.get("json"); err == nil { fs.BoolVar(&c.Options.JSON.SplitStream, "log-json-split-stream", false, "[Alpha] In JSON format, write error messages to stderr and info messages to stdout. The default is to write a single stream to stdout. Enable the LoggingAlphaOptions feature gate to use this.") fs.Var(&c.Options.JSON.InfoBufferSize, "log-json-info-buffer-size", "[Alpha] In JSON format with split output streams, the info messages can be buffered for a while to increase performance. The default value of zero bytes disables buffering. The size can be specified as number of bytes (512), multiples of 1000 (1K), multiples of 1024 (2Ki), or powers of those (3M, 4G, 5Mi, 6Gi). Enable the LoggingAlphaOptions feature gate to use this.") } } // SetRecommendedLoggingConfiguration sets the default logging configuration // for fields that are unset. // // Consumers who embed LoggingConfiguration in their own configuration structs // may set custom defaults and then should call this function to add the // global defaults. func SetRecommendedLoggingConfiguration(c *LoggingConfiguration) { if c.Format == "" { c.Format = "text" } if c.FlushFrequency.Duration.Duration == 0 { c.FlushFrequency.Duration.Duration = LogFlushFreqDefault c.FlushFrequency.SerializeAsString = true } setRecommendedOutputRouting(&c.Options.Text.OutputRoutingOptions) setRecommendedOutputRouting(&c.Options.JSON.OutputRoutingOptions) } func setRecommendedOutputRouting(o *OutputRoutingOptions) { var empty resource.QuantityValue if o.InfoBufferSize == empty { o.InfoBufferSize = resource.QuantityValue{ // This is similar, but not quite the same as a default // constructed instance. Quantity: *resource.NewQuantity(0, resource.DecimalSI), } // This sets the unexported Quantity.s which will be compared // by reflect.DeepEqual in some tests. _ = o.InfoBufferSize.String() } } // loggingFlags captures the state of the logging flags, in particular their default value // before flag parsing. It is used by unsupportedLoggingFlags. var loggingFlags pflag.FlagSet func init() { var fs flag.FlagSet klogflags.Init(&fs) loggingFlags.AddGoFlagSet(&fs) } // List of logs (k8s.io/klog + k8s.io/component-base/logs) flags supported by all logging formats var supportedLogsFlags = map[string]struct{}{ "v": {}, } // unsupportedLoggingFlags lists unsupported logging flags. The normalize // function is optional. func unsupportedLoggingFlags(normalizeFunc func(f *pflag.FlagSet, name string) pflag.NormalizedName) []*pflag.Flag { // k8s.io/component-base/logs and klog flags pfs := &pflag.FlagSet{} loggingFlags.VisitAll(func(flag *pflag.Flag) { if _, found := supportedLogsFlags[flag.Name]; !found { // Normalization changes flag.Name, so make a copy. clone := *flag pfs.AddFlag(&clone) } }) // Apply normalization. pfs.SetNormalizeFunc(normalizeFunc) var allFlags []*pflag.Flag pfs.VisitAll(func(flag *pflag.Flag) { allFlags = append(allFlags, flag) }) return allFlags } kubernetes-component-base-1b2882b/logs/api/v1/options_no_slog.go000066400000000000000000000012671476422213000247370ustar00rootroot00000000000000//go:build !go1.21 // +build !go1.21 /* Copyright 2023 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 v1 func setSlogDefaultLogger() { // Do nothing when build with Go < 1.21. } kubernetes-component-base-1b2882b/logs/api/v1/options_slog.go000066400000000000000000000021221476422213000242320ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 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 v1 import ( "log/slog" "github.com/go-logr/logr" "k8s.io/klog/v2" ) // setSlogDefaultLogger sets the global slog default logger to the same default // that klog currently uses. func setSlogDefaultLogger() { // klog.Background() always returns a valid logr.Logger, regardless of // how logging was configured. We just need to turn it into a // slog.Handler. SetDefault then needs a slog.Logger. handler := logr.ToSlogHandler(klog.Background()) slog.SetDefault(slog.New(handler)) } kubernetes-component-base-1b2882b/logs/api/v1/options_test.go000066400000000000000000000232741476422213000242600ustar00rootroot00000000000000/* Copyright 2021 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 v1 import ( "bytes" "context" "flag" "testing" "github.com/go-logr/logr" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/component-base/featuregate" "k8s.io/klog/v2" ) func TestReapply(t *testing.T) { oldReapplyHandling := ReapplyHandling defer func() { ReapplyHandling = oldReapplyHandling if err := ResetForTest(nil /* feature gates */); err != nil { t.Errorf("Unexpected error resetting the logging configuration: %v", err) } }() newOptions := NewLoggingConfiguration() if err := ValidateAndApply(newOptions, nil); err != nil { t.Errorf("unexpected error for first ValidateAndApply: %v", err) } ReapplyHandling = ReapplyHandlingError if err := ValidateAndApply(newOptions, nil); err == nil { t.Error("did not get expected error for second ValidateAndApply") } ReapplyHandling = ReapplyHandlingIgnoreUnchanged if err := ValidateAndApply(newOptions, nil); err != nil { t.Errorf("unexpected error for third ValidateAndApply: %v", err) } modifiedOptions := newOptions.DeepCopy() modifiedOptions.Verbosity = 100 if err := ValidateAndApply(modifiedOptions, nil); err == nil { t.Errorf("unexpected success for forth ValidateAndApply, should have complained about modified config") } } func TestOptions(t *testing.T) { newOptions := NewLoggingConfiguration() testcases := []struct { name string args []string want *LoggingConfiguration errs field.ErrorList }{ { name: "Default log format", want: newOptions.DeepCopy(), }, { name: "Text log format", args: []string{"--logging-format=text"}, want: newOptions.DeepCopy(), }, { name: "Unsupported log format", args: []string{"--logging-format=test"}, want: func() *LoggingConfiguration { c := newOptions.DeepCopy() c.Format = "test" return c }(), errs: field.ErrorList{&field.Error{ Type: "FieldValueInvalid", Field: "format", BadValue: "test", Detail: "Unsupported log format", }}, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { c := NewLoggingConfiguration() fs := pflag.NewFlagSet("addflagstest", pflag.ContinueOnError) AddFlags(c, fs) fs.Parse(tc.args) if !assert.Equal(t, tc.want, c) { t.Errorf("Wrong Validate() result for %q. expect %v, got %v", tc.name, tc.want, c) } defer func() { if err := ResetForTest(nil /* feature gates */); err != nil { t.Errorf("Unexpected error resetting the logging configuration: %v", err) } }() errs := ValidateAndApply(c, nil /* We don't care about feature gates here. */) defer klog.StopFlushDaemon() if !assert.ElementsMatch(t, tc.errs, errs) { t.Errorf("Wrong Validate() result for %q.\n expect:\t%+v\n got:\t%+v", tc.name, tc.errs, errs) } }) } } func TestFlagSet(t *testing.T) { t.Run("pflag", func(t *testing.T) { newOptions := NewLoggingConfiguration() var fs pflag.FlagSet AddFlags(newOptions, &fs) var buffer bytes.Buffer fs.SetOutput(&buffer) fs.PrintDefaults() // Expected (Go 1.19, pflag v1.0.5): // --logging-format string Sets the log format. Permitted formats: "text". (default "text") // --log-flush-frequency duration Maximum number of seconds between log flushes (default 5s) // -v, --v Level number for the log level verbosity // --vmodule pattern=N,... comma-separated list of pattern=N settings for file-filtered logging (only works for text log format) // --log-text-split-stream [Alpha] In text format, write error messages to stderr and info messages to stdout. The default is to write a single stream to stdout. Enable the LoggingAlphaOptions feature gate to use this. // --log-text-info-buffer-size quantity [Alpha] In text format with split output streams, the info messages can be buffered for a while to increase performance. The default value of zero bytes disables buffering. The size can be specified as number of bytes (512), multiples of 1000 (1K), multiples of 1024 (2Ki), or powers of those (3M, 4G, 5Mi, 6Gi). Enable the LoggingAlphaOptions feature gate to use this. assert.Regexp(t, `^.*--logging-format.*default.*text.* .*--log-flush-frequency.*default 5s.* .*-v.*--v.* .*--vmodule.*pattern=N.* .*--log-text-split-stream.* .*--log-text-info-buffer-size quantity.* $`, buffer.String()) }) t.Run("flag", func(t *testing.T) { newOptions := NewLoggingConfiguration() var pfs pflag.FlagSet AddFlags(newOptions, &pfs) var fs flag.FlagSet pfs.VisitAll(func(f *pflag.Flag) { fs.Var(f.Value, f.Name, f.Usage) }) var buffer bytes.Buffer fs.SetOutput(&buffer) fs.PrintDefaults() // Expected (Go 1.19): // -log-flush-frequency value // Maximum number of seconds between log flushes (default 5s) // -log-text-info-buffer-size value // [Alpha] In text format with split output streams, the info messages can be buffered for a while to increase performance. The default value of zero bytes disables buffering. The size can be specified as number of bytes (512), multiples of 1000 (1K), multiples of 1024 (2Ki), or powers of those (3M, 4G, 5Mi, 6Gi). Enable the LoggingAlphaOptions feature gate to use this. // -log-text-split-stream // [Alpha] In text format, write error messages to stderr and info messages to stdout. The default is to write a single stream to stdout. Enable the LoggingAlphaOptions feature gate to use this. // -logging-format value // Sets the log format. Permitted formats: "text". (default text) // -v value // number for the log level verbosity // -vmodule value // comma-separated list of pattern=N settings for file-filtered logging (only works for text log format) assert.Regexp(t, `^.*-log-flush-frequency.* .*default 5s.* .*-log-text-info-buffer-size.* .* .*-log-text-split-stream.* .* .*-logging-format.* .*default.*text.* .*-v.* .* .*-vmodule.* .* $`, buffer.String()) }) t.Run("AddGoFlags", func(t *testing.T) { newOptions := NewLoggingConfiguration() var fs flag.FlagSet var buffer bytes.Buffer AddGoFlags(newOptions, &fs) fs.SetOutput(&buffer) fs.PrintDefaults() // In contrast to copying through VisitAll, the type of some options is now // known: // -log-flush-frequency duration // Maximum number of seconds between log flushes (default 5s) // -log-text-info-buffer-size value // [Alpha] In text format with split output streams, the info messages can be buffered for a while to increase performance. The default value of zero bytes disables buffering. The size can be specified as number of bytes (512), multiples of 1000 (1K), multiples of 1024 (2Ki), or powers of those (3M, 4G, 5Mi, 6Gi). Enable the LoggingAlphaOptions feature gate to use this. // -log-text-split-stream // [Alpha] In text format, write error messages to stderr and info messages to stdout. The default is to write a single stream to stdout. Enable the LoggingAlphaOptions feature gate to use this. // -logging-format string // Sets the log format. Permitted formats: "text". (default "text") // -v value // number for the log level verbosity // -vmodule value // comma-separated list of pattern=N settings for file-filtered logging (only works for text log format) assert.Regexp(t, `^.*-log-flush-frequency.*duration.* .*default 5s.* .*-log-text-info-buffer-size.* .* .*-log-text-split-stream.* .* .*-logging-format.*string.* .*default.*text.* .*-v.* .* .*-vmodule.* .* $`, buffer.String()) }) } func TestContextualLogging(t *testing.T) { t.Run("enabled", func(t *testing.T) { testContextualLogging(t, true) }) t.Run("disabled", func(t *testing.T) { testContextualLogging(t, false) }) } func testContextualLogging(t *testing.T, enabled bool) { var err error c := NewLoggingConfiguration() featureGate := featuregate.NewFeatureGate() AddFeatureGates(featureGate) err = featureGate.SetFromMap(map[string]bool{string(ContextualLogging): enabled}) require.NoError(t, err) defer func() { if err := ResetForTest(nil /* feature gates */); err != nil { t.Errorf("Unexpected error resetting the logging configuration: %v", err) } }() err = ValidateAndApply(c, featureGate) require.NoError(t, err) defer klog.StopFlushDaemon() defer klog.EnableContextualLogging(true) ctx := context.Background() // nolint:logcheck // This intentionally adds a name independently of the feature gate. logger := klog.NewKlogr().WithName("contextual") // nolint:logcheck // This intentionally creates a new context independently of the feature gate. ctx = logr.NewContext(ctx, logger) if enabled { assert.Equal(t, logger, klog.FromContext(ctx), "FromContext") assert.NotEqual(t, ctx, klog.NewContext(ctx, logger), "NewContext") assert.NotEqual(t, logger, klog.LoggerWithName(logger, "foo"), "LoggerWithName") assert.NotEqual(t, logger, klog.LoggerWithValues(logger, "x", "y"), "LoggerWithValues") } else { assert.NotEqual(t, logger, klog.FromContext(ctx), "FromContext") assert.Equal(t, ctx, klog.NewContext(ctx, logger), "NewContext") assert.Equal(t, logger, klog.LoggerWithName(logger, "foo"), "LoggerWithName") assert.Equal(t, logger, klog.LoggerWithValues(logger, "x", "y"), "LoggerWithValues") } } kubernetes-component-base-1b2882b/logs/api/v1/pflags.go000066400000000000000000000062131476422213000227740ustar00rootroot00000000000000/* Copyright 2021 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 v1 import ( "fmt" "strconv" "strings" "github.com/spf13/pflag" ) // VModuleConfigurationPflag implements the pflag.Value interface for a // VModuleConfiguration. The value pointer must not be nil. func VModuleConfigurationPflag(value *VModuleConfiguration) pflag.Value { return vmoduleConfigurationPFlag{value} } type vmoduleConfigurationPFlag struct { value *VModuleConfiguration } // String returns the -vmodule parameter (comma-separated list of pattern=N). func (wrapper vmoduleConfigurationPFlag) String() string { if wrapper.value == nil { return "" } var patterns []string for _, item := range *wrapper.value { patterns = append(patterns, fmt.Sprintf("%s=%d", item.FilePattern, item.Verbosity)) } return strings.Join(patterns, ",") } // Set parses the -vmodule parameter (comma-separated list of pattern=N). func (wrapper vmoduleConfigurationPFlag) Set(value string) error { // This code mirrors https://github.com/kubernetes/klog/blob/9ad246211af1ed84621ee94a26fcce0038b69cd1/klog.go#L287-L313 for _, pat := range strings.Split(value, ",") { if len(pat) == 0 { // Empty strings such as from a trailing comma can be ignored. continue } patLev := strings.Split(pat, "=") if len(patLev) != 2 || len(patLev[0]) == 0 || len(patLev[1]) == 0 { return fmt.Errorf("%q does not have the pattern=N format", pat) } pattern := patLev[0] // 31 instead of 32 to ensure that it also fits into int32. v, err := strconv.ParseUint(patLev[1], 10, 31) if err != nil { return fmt.Errorf("parsing verbosity in %q: %v", pat, err) } *wrapper.value = append(*wrapper.value, VModuleItem{FilePattern: pattern, Verbosity: VerbosityLevel(v)}) } return nil } func (wrapper vmoduleConfigurationPFlag) Type() string { return "pattern=N,..." } // VerbosityLevelPflag implements the pflag.Value interface for a verbosity // level value. func VerbosityLevelPflag(value *VerbosityLevel) pflag.Value { return verbosityLevelPflag{value} } type verbosityLevelPflag struct { value *VerbosityLevel } func (wrapper verbosityLevelPflag) String() string { if wrapper.value == nil { return "0" } return strconv.FormatInt(int64(*wrapper.value), 10) } func (wrapper verbosityLevelPflag) Get() interface{} { if wrapper.value == nil { return VerbosityLevel(0) } return *wrapper.value } func (wrapper verbosityLevelPflag) Set(value string) error { // Limited to int32 for compatibility with klog. v, err := strconv.ParseUint(value, 10, 31) if err != nil { return err } *wrapper.value = VerbosityLevel(v) return nil } func (wrapper verbosityLevelPflag) Type() string { return "Level" } kubernetes-component-base-1b2882b/logs/api/v1/registry.go000066400000000000000000000101121476422213000233610ustar00rootroot00000000000000/* Copyright 2020 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 v1 import ( "fmt" "sort" "strings" "sync" "github.com/go-logr/logr" "k8s.io/component-base/featuregate" ) var logRegistry = newLogFormatRegistry() // logFormatRegistry stores factories for all supported logging formats. type logFormatRegistry struct { mutex sync.Mutex registry map[string]logFormat frozen bool } type logFormat struct { factory LogFormatFactory feature featuregate.Feature } // +k8s:deepcopy-gen=false // RuntimeControl provides operations that aren't available through the normal // Logger or LogSink API. type RuntimeControl struct { // Flush ensures that all in-memory data is written. // May be nil. Flush func() // SetVerbosityLevel changes the level for all Logger instances // derived from the initial one. May be nil. // // The parameter is intentionally a plain uint32 instead of // VerbosityLevel to enable implementations that don't need to import // the API (helps avoid circular dependencies). SetVerbosityLevel func(v uint32) error } // LogFormatFactory provides support for a certain additional, // non-default log format. type LogFormatFactory interface { // Create returns a logger with the requested configuration. Create(c LoggingConfiguration, o LoggingOptions) (logr.Logger, RuntimeControl) } // RegisterLogFormat registers support for a new logging format. This must be called // before using any of the methods in LoggingConfiguration. The feature must // be one of those defined in this package (typically LoggingAlphaOptions, // LoggingBetaOptions or LoggingStableOptions). func RegisterLogFormat(name string, factory LogFormatFactory, feature featuregate.Feature) error { return logRegistry.register(name, logFormat{factory, feature}) } func newLogFormatRegistry() *logFormatRegistry { registry := &logFormatRegistry{ registry: make(map[string]logFormat), frozen: false, } _ = registry.register(DefaultLogFormat, logFormat{factory: textFactory{}, feature: LoggingStableOptions}) return registry } // register adds a new log format. It's an error to modify an existing one. func (lfr *logFormatRegistry) register(name string, format logFormat) error { lfr.mutex.Lock() defer lfr.mutex.Unlock() if lfr.frozen { return fmt.Errorf("log format registry is frozen, unable to register log format %s", name) } if _, ok := lfr.registry[name]; ok { return fmt.Errorf("log format: %s already exists", name) } if _, ok := featureGates()[format.feature]; !ok && format.feature != LoggingStableOptions { return fmt.Errorf("log format %s: unsupported feature gate %s", name, format.feature) } lfr.registry[name] = format return nil } // get specified log format factory func (lfr *logFormatRegistry) get(name string) (*logFormat, error) { lfr.mutex.Lock() defer lfr.mutex.Unlock() format, ok := lfr.registry[name] if !ok { return nil, fmt.Errorf("log format: %s does not exists", name) } return &format, nil } // list names of registered log formats, including feature gates (sorted) func (lfr *logFormatRegistry) list() string { lfr.mutex.Lock() defer lfr.mutex.Unlock() formats := make([]string, 0, len(lfr.registry)) for name, format := range lfr.registry { item := fmt.Sprintf(`"%s"`, name) if format.feature != LoggingStableOptions { item += fmt.Sprintf(" (gated by %s)", format.feature) } formats = append(formats, item) } sort.Strings(formats) return strings.Join(formats, ", ") } // freeze prevents further modifications of the registered log formats. func (lfr *logFormatRegistry) freeze() { lfr.mutex.Lock() defer lfr.mutex.Unlock() lfr.frozen = true } kubernetes-component-base-1b2882b/logs/api/v1/text.go000066400000000000000000000065401476422213000225070ustar00rootroot00000000000000/* Copyright 2022 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 v1 import ( "bufio" "fmt" "io" "sync" "github.com/go-logr/logr" "k8s.io/component-base/featuregate" "k8s.io/klog/v2/textlogger" ) // textFactory produces klog text logger instances. type textFactory struct{} var _ LogFormatFactory = textFactory{} func (f textFactory) Feature() featuregate.Feature { return LoggingStableOptions } func (f textFactory) Create(c LoggingConfiguration, o LoggingOptions) (logr.Logger, RuntimeControl) { output := o.ErrorStream var flush func() if c.Options.Text.SplitStream { r := &klogMsgRouter{ info: o.InfoStream, error: o.ErrorStream, } size := c.Options.Text.InfoBufferSize.Value() if size > 0 { // Prevent integer overflow. if size > 2*1024*1024*1024 { size = 2 * 1024 * 1024 * 1024 } info := newBufferedWriter(r.info, int(size)) flush = info.Flush r.info = info } output = r } options := []textlogger.ConfigOption{ textlogger.Verbosity(int(c.Verbosity)), textlogger.Output(output), } loggerConfig := textlogger.NewConfig(options...) // This should never fail, we produce a valid string here. _ = loggerConfig.VModule().Set(VModuleConfigurationPflag(&c.VModule).String()) return textlogger.NewLogger(loggerConfig), RuntimeControl{ SetVerbosityLevel: func(v uint32) error { return loggerConfig.Verbosity().Set(fmt.Sprintf("%d", v)) }, Flush: flush, } } type klogMsgRouter struct { info, error io.Writer } var _ io.Writer = &klogMsgRouter{} // Write redirects the message into either the info or error // stream, depending on its type as indicated in text format // by the first byte. func (r *klogMsgRouter) Write(p []byte) (int, error) { if len(p) == 0 { return 0, nil } if p[0] == 'I' { return r.info.Write(p) } return r.error.Write(p) } // bufferedWriter is an io.Writer that buffers writes in-memory before // flushing them to a wrapped io.Writer after reaching some limit // or getting flushed. type bufferedWriter struct { mu sync.Mutex writer *bufio.Writer out io.Writer } func newBufferedWriter(out io.Writer, size int) *bufferedWriter { return &bufferedWriter{ writer: bufio.NewWriterSize(out, size), out: out, } } func (b *bufferedWriter) Write(p []byte) (int, error) { b.mu.Lock() defer b.mu.Unlock() // To avoid partial writes into the underlying writer, we ensure that // the entire new data fits into the buffer or flush first. if len(p) > b.writer.Available() && b.writer.Buffered() > 0 { if err := b.writer.Flush(); err != nil { return 0, err } } // If it still doesn't fit, then we bypass the now empty buffer // and write directly. if len(p) > b.writer.Available() { return b.out.Write(p) } // This goes into the buffer. return b.writer.Write(p) } func (b *bufferedWriter) Flush() { b.mu.Lock() defer b.mu.Unlock() _ = b.writer.Flush() } kubernetes-component-base-1b2882b/logs/api/v1/types.go000066400000000000000000000127061476422213000226700ustar00rootroot00000000000000/* Copyright 2021 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 v1 import ( "encoding/json" "fmt" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Supported output formats. const ( // DefaultLogFormat is the traditional klog output format. DefaultLogFormat = "text" // JSONLogFormat emits each log message as a JSON struct. JSONLogFormat = "json" ) // The alpha or beta level of structs is the highest stability level of any field // inside it. Feature gates will get checked during LoggingConfiguration.ValidateAndApply. // LoggingConfiguration contains logging options. type LoggingConfiguration struct { // Format Flag specifies the structure of log messages. // default value of format is `text` Format string `json:"format,omitempty"` // Maximum time between log flushes. // If a string, parsed as a duration (i.e. "1s") // If an int, the maximum number of nanoseconds (i.e. 1s = 1000000000). // Ignored if the selected logging backend writes log messages without buffering. FlushFrequency TimeOrMetaDuration `json:"flushFrequency"` // Verbosity is the threshold that determines which log messages are // logged. Default is zero which logs only the most important // messages. Higher values enable additional messages. Error messages // are always logged. Verbosity VerbosityLevel `json:"verbosity"` // VModule overrides the verbosity threshold for individual files. // Only supported for "text" log format. VModule VModuleConfiguration `json:"vmodule,omitempty"` // [Alpha] Options holds additional parameters that are specific // to the different logging formats. Only the options for the selected // format get used, but all of them get validated. // Only available when the LoggingAlphaOptions feature gate is enabled. Options FormatOptions `json:"options,omitempty"` } // TimeOrMetaDuration is present only for backwards compatibility for the // flushFrequency field, and new fields should use metav1.Duration. type TimeOrMetaDuration struct { // Duration holds the duration Duration metav1.Duration // SerializeAsString controls whether the value is serialized as a string or an integer SerializeAsString bool `json:"-"` } func (t TimeOrMetaDuration) MarshalJSON() ([]byte, error) { if t.SerializeAsString { return t.Duration.MarshalJSON() } else { // Marshal as integer for backwards compatibility return json.Marshal(t.Duration.Duration) } } func (t *TimeOrMetaDuration) UnmarshalJSON(b []byte) error { if len(b) > 0 && b[0] == '"' { // string values unmarshal as metav1.Duration t.SerializeAsString = true return json.Unmarshal(b, &t.Duration) } t.SerializeAsString = false if err := json.Unmarshal(b, &t.Duration.Duration); err != nil { return fmt.Errorf("invalid duration %q: %w", string(b), err) } return nil } // FormatOptions contains options for the different logging formats. type FormatOptions struct { // [Alpha] Text contains options for logging format "text". // Only available when the LoggingAlphaOptions feature gate is enabled. Text TextOptions `json:"text,omitempty"` // [Alpha] JSON contains options for logging format "json". // Only available when the LoggingAlphaOptions feature gate is enabled. JSON JSONOptions `json:"json,omitempty"` } // TextOptions contains options for logging format "text". type TextOptions struct { OutputRoutingOptions `json:",inline"` } // JSONOptions contains options for logging format "json". type JSONOptions struct { OutputRoutingOptions `json:",inline"` } // OutputRoutingOptions contains options that are supported by both "text" and "json". type OutputRoutingOptions struct { // [Alpha] SplitStream redirects error messages to stderr while // info messages go to stdout, with buffering. The default is to write // both to stdout, without buffering. Only available when // the LoggingAlphaOptions feature gate is enabled. SplitStream bool `json:"splitStream,omitempty"` // [Alpha] InfoBufferSize sets the size of the info stream when // using split streams. The default is zero, which disables buffering. // Only available when the LoggingAlphaOptions feature gate is enabled. InfoBufferSize resource.QuantityValue `json:"infoBufferSize,omitempty"` } // VModuleConfiguration is a collection of individual file names or patterns // and the corresponding verbosity threshold. type VModuleConfiguration []VModuleItem // VModuleItem defines verbosity for one or more files which match a certain // glob pattern. type VModuleItem struct { // FilePattern is a base file name (i.e. minus the ".go" suffix and // directory) or a "glob" pattern for such a name. It must not contain // comma and equal signs because those are separators for the // corresponding klog command line argument. FilePattern string `json:"filePattern"` // Verbosity is the threshold for log messages emitted inside files // that match the pattern. Verbosity VerbosityLevel `json:"verbosity"` } // VerbosityLevel represents a klog or logr verbosity threshold. type VerbosityLevel uint32 kubernetes-component-base-1b2882b/logs/api/v1/types_test.go000066400000000000000000000210311476422213000237160ustar00rootroot00000000000000/* Copyright 2022 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 v1 import ( enjson "encoding/json" "fmt" "math" "reflect" "testing" "time" "github.com/stretchr/testify/assert" "sigs.k8s.io/json" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestVModule(t *testing.T) { testcases := []struct { arg string expectError string expectValue VModuleConfiguration expectParam string }{ { arg: "gopher*=1", expectValue: VModuleConfiguration{ { FilePattern: "gopher*", Verbosity: 1, }, }, }, { arg: "foo=1,bar=2", expectValue: VModuleConfiguration{ { FilePattern: "foo", Verbosity: 1, }, { FilePattern: "bar", Verbosity: 2, }, }, }, { arg: "foo=1,bar=2,", expectValue: VModuleConfiguration{ { FilePattern: "foo", Verbosity: 1, }, { FilePattern: "bar", Verbosity: 2, }, }, expectParam: "foo=1,bar=2", }, { arg: "gopher*", expectError: `"gopher*" does not have the pattern=N format`, }, { arg: "=1", expectError: `"=1" does not have the pattern=N format`, }, { arg: "foo=-1", expectError: `parsing verbosity in "foo=-1": strconv.ParseUint: parsing "-1": invalid syntax`, }, { arg: fmt.Sprintf("validint32=%d", math.MaxInt32), expectValue: VModuleConfiguration{ { FilePattern: "validint32", Verbosity: math.MaxInt32, }, }, }, { arg: fmt.Sprintf("invalidint32=%d", uint(math.MaxInt32+1)), expectError: `parsing verbosity in "invalidint32=2147483648": strconv.ParseUint: parsing "2147483648": value out of range`, }, } for _, test := range testcases { t.Run(test.arg, func(t *testing.T) { var actual VModuleConfiguration value := VModuleConfigurationPflag(&actual) err := value.Set(test.arg) if test.expectError != "" { if err == nil { t.Fatal("parsing should have failed") } assert.Equal(t, test.expectError, err.Error(), "parse error") } else { if err != nil { t.Fatalf("unexpected error: %v", err) } param := value.String() expectParam := test.expectParam if expectParam == "" { expectParam = test.arg } assert.Equal(t, expectParam, param, "encoded parameter value not identical") } }) } } // TestCompatibility ensures that a) valid JSON remains valid and has the same // effect and b) that new fields are covered by the test data. func TestCompatibility(t *testing.T) { testcases := map[string]struct { // fixture holds a representation of a LoggingConfiguration struct in JSON format. fixture string // baseConfig is the struct that Unmarshal writes into. baseConfig LoggingConfiguration // expectAllFields enables a reflection check to ensure that the // result has all fields set. expectAllFields bool // expectConfig is the intended result. expectConfig LoggingConfiguration }{ "defaults": { // No changes when nothing is specified. fixture: "{}", baseConfig: *NewLoggingConfiguration(), expectConfig: *NewLoggingConfiguration(), }, "all-fields": { // The JSON fixture includes all fields. The result // must have all fields as non-empty when starting with // an empty base, otherwise the fixture is incomplete // and must be updated for the test case to pass. fixture: `{ "format": "json", "flushFrequency": 1, "verbosity": 5, "vmodule": [ {"filePattern": "someFile", "verbosity": 10}, {"filePattern": "anotherFile", "verbosity": 1} ], "options": { "text": { "splitStream": true, "infoBufferSize": "2048" }, "json": { "splitStream": true, "infoBufferSize": "1024" } } } `, baseConfig: LoggingConfiguration{}, expectAllFields: true, expectConfig: LoggingConfiguration{ Format: JSONLogFormat, FlushFrequency: TimeOrMetaDuration{Duration: metav1.Duration{Duration: time.Nanosecond}}, Verbosity: VerbosityLevel(5), VModule: VModuleConfiguration{ { FilePattern: "someFile", Verbosity: VerbosityLevel(10), }, { FilePattern: "anotherFile", Verbosity: VerbosityLevel(1), }, }, Options: FormatOptions{ Text: TextOptions{ OutputRoutingOptions: OutputRoutingOptions{ SplitStream: true, InfoBufferSize: resource.QuantityValue{ Quantity: *resource.NewQuantity(2048, resource.DecimalSI), }, }, }, JSON: JSONOptions{ OutputRoutingOptions: OutputRoutingOptions{ SplitStream: true, InfoBufferSize: resource.QuantityValue{ Quantity: *resource.NewQuantity(1024, resource.DecimalSI), }, }, }, }, }, }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { // Beware, not a deep copy. Different test cases must // not share anything. config := tc.baseConfig if strictErr, err := json.UnmarshalStrict([]byte(tc.fixture), &config); err != nil { t.Fatalf("unexpected unmarshal error: %v", err) } else if strictErr != nil { t.Fatalf("unexpected strict unmarshal error: %v", strictErr) } // This sets the internal "s" field just like unmarshaling does. // Required for assert.Equal to pass. _ = tc.expectConfig.Options.Text.InfoBufferSize.String() _ = tc.expectConfig.Options.JSON.InfoBufferSize.String() assert.Equal(t, tc.expectConfig, config) if tc.expectAllFields { notZeroRecursive(t, config, "LoggingConfiguration") } }) } } // notZero asserts that i is not the zero value for its type // and repeats that check recursively for all pointers, // structs, maps, arrays, and slices. func notZeroRecursive(t *testing.T, i interface{}, path string) bool { typeOfI := reflect.TypeOf(i) if i == nil || reflect.DeepEqual(i, reflect.Zero(typeOfI).Interface()) { t.Errorf("%s: should not have been zero, but was %v", path, i) return false } valid := true kind := typeOfI.Kind() value := reflect.ValueOf(i) switch kind { case reflect.Pointer: if !notZeroRecursive(t, value.Elem().Interface(), path) { valid = false } case reflect.Struct: for i := 0; i < typeOfI.NumField(); i++ { if !typeOfI.Field(i).IsExported() { // Cannot access value. continue } if typeOfI.Field(i).Tag.Get("json") == "-" { // unserialized field continue } if !notZeroRecursive(t, value.Field(i).Interface(), path+"."+typeOfI.Field(i).Name) { valid = false } } case reflect.Map: iter := value.MapRange() for iter.Next() { k := iter.Key() v := iter.Value() if !notZeroRecursive(t, k.Interface(), path+"."+"") { valid = false } if !notZeroRecursive(t, v.Interface(), path+"["+fmt.Sprintf("%v", k.Interface())+"]") { valid = false } } case reflect.Slice, reflect.Array: for i := 0; i < value.Len(); i++ { if !notZeroRecursive(t, value.Index(i).Interface(), path+"["+fmt.Sprintf("%d", i)+"]") { valid = false } } } return valid } func TestTimeOrMetaDuration_UnmarshalJSON(t *testing.T) { tests := []struct { name string tomd *TimeOrMetaDuration arg any wanted string }{ { name: "string values unmarshal as metav1.Duration", tomd: &TimeOrMetaDuration{}, arg: "1s", wanted: `"1s"`, }, { name: "int values unmarshal as metav1.Duration", tomd: &TimeOrMetaDuration{}, arg: 1000000000, wanted: `1000000000`, }, { name: "invalid value return error", tomd: &TimeOrMetaDuration{}, arg: "invalid", wanted: "time: invalid duration \"invalid\"", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b, err := enjson.Marshal(tt.arg) if err != nil { t.Errorf("unexpect error: %v", err) } if err := tt.tomd.UnmarshalJSON(b); err == nil { data, err := tt.tomd.MarshalJSON() if err != nil { t.Fatal(err) } if tt.wanted != string(data) { t.Errorf("unexpected wanted for %s, wanted: %v, got: %v", tt.name, tt.wanted, string(data)) } } else { if err.Error() != tt.wanted { t.Errorf("UnmarshalJSON() error = %v", err) } } }) } } kubernetes-component-base-1b2882b/logs/api/v1/validate_test.go000066400000000000000000000106711476422213000243530ustar00rootroot00000000000000/* Copyright 2021 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 v1 import ( "math" "testing" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/component-base/featuregate" ) func TestValidation(t *testing.T) { jsonOptionsEnabled := LoggingConfiguration{ Format: "text", Options: FormatOptions{ JSON: JSONOptions{ OutputRoutingOptions: OutputRoutingOptions{ SplitStream: true, InfoBufferSize: resource.QuantityValue{ Quantity: *resource.NewQuantity(1024, resource.DecimalSI), }, }, }, }, } jsonErrors := `[options.json.splitStream: Forbidden: Feature LoggingAlphaOptions is disabled, options.json.infoBufferSize: Forbidden: Feature LoggingAlphaOptions is disabled]` testcases := map[string]struct { config LoggingConfiguration path *field.Path featureGate featuregate.FeatureGate expectErrors string }{ "okay": { config: LoggingConfiguration{ Format: "text", Verbosity: 10, VModule: VModuleConfiguration{ { FilePattern: "gopher*", Verbosity: 100, }, }, }, }, "wrong-format": { config: LoggingConfiguration{ Format: "no-such-format", }, expectErrors: `format: Invalid value: "no-such-format": Unsupported log format`, }, "embedded": { config: LoggingConfiguration{ Format: "no-such-format", }, path: field.NewPath("config"), expectErrors: `config.format: Invalid value: "no-such-format": Unsupported log format`, }, "verbosity-overflow": { config: LoggingConfiguration{ Format: "text", Verbosity: math.MaxInt32 + 1, }, expectErrors: `verbosity: Invalid value: 0x80000000: Must be <= 2147483647`, }, "vmodule-verbosity-overflow": { config: LoggingConfiguration{ Format: "text", VModule: VModuleConfiguration{ { FilePattern: "gopher*", Verbosity: math.MaxInt32 + 1, }, }, }, expectErrors: `vmodule[0]: Invalid value: 0x80000000: Must be <= 2147483647`, }, "vmodule-empty-pattern": { config: LoggingConfiguration{ Format: "text", VModule: VModuleConfiguration{ { FilePattern: "", Verbosity: 1, }, }, }, expectErrors: `vmodule[0]: Required value: File pattern must not be empty`, }, "vmodule-pattern-with-special-characters": { config: LoggingConfiguration{ Format: "text", VModule: VModuleConfiguration{ { FilePattern: "foo,bar", Verbosity: 1, }, { FilePattern: "foo=bar", Verbosity: 1, }, }, }, expectErrors: `[vmodule[0]: Invalid value: "foo,bar": File pattern must not contain equal sign or comma, vmodule[1]: Invalid value: "foo=bar": File pattern must not contain equal sign or comma]`, }, "vmodule-unsupported": { config: LoggingConfiguration{ Format: "json", VModule: VModuleConfiguration{ { FilePattern: "foo", Verbosity: 1, }, }, }, expectErrors: `[format: Invalid value: "json": Unsupported log format, vmodule: Forbidden: Only supported for text log format]`, }, "JSON used, default gates": { config: jsonOptionsEnabled, featureGate: defaultFeatureGate, expectErrors: jsonErrors, }, "JSON used, disabled gates": { config: jsonOptionsEnabled, featureGate: disabledFeatureGate, expectErrors: jsonErrors, }, "JSON used, enabled gates": { config: jsonOptionsEnabled, featureGate: enabledFeatureGate, }, } for name, test := range testcases { t.Run(name, func(t *testing.T) { featureGate := test.featureGate if featureGate == nil { featureGate = defaultFeatureGate } err := Validate(&test.config, featureGate, test.path) if len(err) == 0 { if test.expectErrors != "" { t.Fatalf("did not get expected error(s): %s", test.expectErrors) } } else { assert.Equal(t, test.expectErrors, err.ToAggregate().Error()) } }) } } kubernetes-component-base-1b2882b/logs/api/v1/zz_generated.deepcopy.go000066400000000000000000000115471476422213000260160ustar00rootroot00000000000000//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 v1 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FormatOptions) DeepCopyInto(out *FormatOptions) { *out = *in in.Text.DeepCopyInto(&out.Text) in.JSON.DeepCopyInto(&out.JSON) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FormatOptions. func (in *FormatOptions) DeepCopy() *FormatOptions { if in == nil { return nil } out := new(FormatOptions) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JSONOptions) DeepCopyInto(out *JSONOptions) { *out = *in in.OutputRoutingOptions.DeepCopyInto(&out.OutputRoutingOptions) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JSONOptions. func (in *JSONOptions) DeepCopy() *JSONOptions { if in == nil { return nil } out := new(JSONOptions) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LoggingConfiguration) DeepCopyInto(out *LoggingConfiguration) { *out = *in out.FlushFrequency = in.FlushFrequency if in.VModule != nil { in, out := &in.VModule, &out.VModule *out = make(VModuleConfiguration, len(*in)) copy(*out, *in) } in.Options.DeepCopyInto(&out.Options) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoggingConfiguration. func (in *LoggingConfiguration) DeepCopy() *LoggingConfiguration { if in == nil { return nil } out := new(LoggingConfiguration) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OutputRoutingOptions) DeepCopyInto(out *OutputRoutingOptions) { *out = *in in.InfoBufferSize.DeepCopyInto(&out.InfoBufferSize) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OutputRoutingOptions. func (in *OutputRoutingOptions) DeepCopy() *OutputRoutingOptions { if in == nil { return nil } out := new(OutputRoutingOptions) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TextOptions) DeepCopyInto(out *TextOptions) { *out = *in in.OutputRoutingOptions.DeepCopyInto(&out.OutputRoutingOptions) return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TextOptions. func (in *TextOptions) DeepCopy() *TextOptions { if in == nil { return nil } out := new(TextOptions) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TimeOrMetaDuration) DeepCopyInto(out *TimeOrMetaDuration) { *out = *in out.Duration = in.Duration return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TimeOrMetaDuration. func (in *TimeOrMetaDuration) DeepCopy() *TimeOrMetaDuration { if in == nil { return nil } out := new(TimeOrMetaDuration) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in VModuleConfiguration) DeepCopyInto(out *VModuleConfiguration) { { in := &in *out = make(VModuleConfiguration, len(*in)) copy(*out, *in) return } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VModuleConfiguration. func (in VModuleConfiguration) DeepCopy() VModuleConfiguration { if in == nil { return nil } out := new(VModuleConfiguration) in.DeepCopyInto(out) return *out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VModuleItem) DeepCopyInto(out *VModuleItem) { *out = *in return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VModuleItem. func (in *VModuleItem) DeepCopy() *VModuleItem { if in == nil { return nil } out := new(VModuleItem) in.DeepCopyInto(out) return out } kubernetes-component-base-1b2882b/logs/datapol/000077500000000000000000000000001476422213000215145ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/datapol/datapol.go000066400000000000000000000046061476422213000234750ustar00rootroot00000000000000/* Copyright 2020 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 datapol contains functions to determine if objects contain sensitive // data to e.g. make decisions on whether to log them or not. package datapol import ( "reflect" "strings" "k8s.io/klog/v2" ) // Verify returns a list of the datatypes contained in the argument that can be // considered sensitive w.r.t. to logging func Verify(value interface{}) []string { defer func() { if r := recover(); r != nil { //TODO maybe export a metric klog.Warningf("Error while inspecting arguments for sensitive data: %v", r) } }() t := reflect.ValueOf(value) if t.Kind() == reflect.Pointer { t = t.Elem() } return datatypes(t) } func datatypes(v reflect.Value) []string { if types := byType(v.Type()); len(types) > 0 { // Slices, and maps can be nil or empty, only the nil case is zero switch v.Kind() { case reflect.Slice, reflect.Map: if !v.IsZero() && v.Len() > 0 { return types } default: if !v.IsZero() { return types } } } switch v.Kind() { case reflect.Interface: return datatypes(v.Elem()) case reflect.Slice, reflect.Array: for i := 0; i < v.Len(); i++ { if types := datatypes(v.Index(i)); len(types) > 0 { return types } } case reflect.Map: mapIter := v.MapRange() for mapIter.Next() { k := mapIter.Key() v := mapIter.Value() if types := datatypes(k); len(types) > 0 { return types } if types := datatypes(v); len(types) > 0 { return types } } case reflect.Struct: t := v.Type() numField := t.NumField() for i := 0; i < numField; i++ { f := t.Field(i) if f.Type.Kind() == reflect.Pointer { continue } if reason, ok := f.Tag.Lookup("datapolicy"); ok { if !v.Field(i).IsZero() { return strings.Split(reason, ",") } } if types := datatypes(v.Field(i)); len(types) > 0 { return types } } } return nil } kubernetes-component-base-1b2882b/logs/datapol/datapol_test.go000066400000000000000000000064501476422213000245330ustar00rootroot00000000000000/* Copyright 2020 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 datapol import ( "fmt" "net/http" "strings" "testing" "github.com/stretchr/testify/assert" ) const ( marker = "hunter2" ) type withDatapolTag struct { Key string `json:"key" datapolicy:"password"` } type withExternalType struct { Header http.Header `json:"header"` } type noDatapol struct { Key string `json:"key"` } type datapolInMember struct { secrets withDatapolTag } type datapolInSlice struct { secrets []withDatapolTag } type datapolInMap struct { secrets map[string]withDatapolTag } type datapolBehindPointer struct { secrets *withDatapolTag } func TestValidate(t *testing.T) { testcases := []struct { name string value interface{} expect []string badFilter bool }{{ name: "Empty password", value: withDatapolTag{}, expect: []string{}, }, { name: "Non-empty password", value: withDatapolTag{ Key: marker, }, expect: []string{"password"}, }, { name: "empty external type", value: withExternalType{Header: http.Header{}}, expect: []string{}, }, { name: "external type", value: withExternalType{Header: http.Header{ "Authorization": []string{"Bearer hunter2"}, }}, expect: []string{"password", "token"}, }, { name: "no datapol tag", value: noDatapol{Key: marker}, expect: []string{}, badFilter: true, }, { name: "nested", value: datapolInMember{ secrets: withDatapolTag{ Key: marker, }, }, expect: []string{"password"}, }, { name: "nested in pointer", value: datapolBehindPointer{ secrets: &withDatapolTag{Key: marker}, }, expect: []string{}, }, { name: "nested in slice", value: datapolInSlice{ secrets: []withDatapolTag{{Key: marker}}, }, expect: []string{"password"}, }, { name: "nested in map", value: datapolInMap{ secrets: map[string]withDatapolTag{ "key": {Key: marker}, }, }, expect: []string{"password"}, }, { name: "nested in map but empty", value: datapolInMap{ secrets: map[string]withDatapolTag{ "key": {}, }, }, expect: []string{}, }, { name: "struct in interface", value: struct{ v interface{} }{v: withDatapolTag{ Key: marker, }}, expect: []string{"password"}, }, { name: "structptr in interface", value: struct{ v interface{} }{v: &withDatapolTag{ Key: marker, }}, expect: []string{}, }} for _, tc := range testcases { res := Verify(tc.value) if !assert.ElementsMatch(t, tc.expect, res) { t.Errorf("Wrong set of tags for %q. expect %v, got %v", tc.name, tc.expect, res) } if !tc.badFilter { formatted := fmt.Sprintf("%v", tc.value) if strings.Contains(formatted, marker) != (len(tc.expect) > 0) { t.Errorf("Filter decision doesn't match formatted value for %q: tags: %v, format: %s", tc.name, tc.expect, formatted) } } } } kubernetes-component-base-1b2882b/logs/datapol/externaltypes.go000066400000000000000000000024611476422213000247550ustar00rootroot00000000000000/* Copyright 2020 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 datapol import ( "fmt" "reflect" ) const ( httpHeader = "net/http.Header" httpCookie = "net/http.Cookie" x509Certificate = "crypto/x509.Certificate" ) // GlobalDatapolicyMapping returns the list of sensitive datatypes are embedded // in types not native to Kubernetes. func GlobalDatapolicyMapping(v interface{}) []string { return byType(reflect.TypeOf(v)) } func byType(t reflect.Type) []string { // Use string representation of the type to prevent taking a depency on the actual type. switch fmt.Sprintf("%s.%s", t.PkgPath(), t.Name()) { case httpHeader: return []string{"password", "token"} case httpCookie: return []string{"token"} case x509Certificate: return []string{"security-key"} default: return nil } } kubernetes-component-base-1b2882b/logs/datapol/externaltypes_test.go000066400000000000000000000022771476422213000260210ustar00rootroot00000000000000/* Copyright 2020 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 datapol import ( "crypto/x509" "net/http" "testing" "github.com/stretchr/testify/assert" ) func TestTypes(t *testing.T) { testcases := []struct { value interface{} expect []string }{{ value: http.Header{}, expect: []string{"password", "token"}, }, { value: http.Cookie{}, expect: []string{"token"}, }, { value: x509.Certificate{}, expect: []string{"security-key"}, }} for _, tc := range testcases { types := GlobalDatapolicyMapping(tc.value) if !assert.ElementsMatch(t, tc.expect, types) { t.Errorf("Wrong set of datatypes detected for %T, want: %v, got %v", tc.value, tc.expect, types) } } } kubernetes-component-base-1b2882b/logs/example/000077500000000000000000000000001476422213000215235ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/example/README.md000066400000000000000000000115631476422213000230100ustar00rootroot00000000000000# Example This directory includes example logger binaries which show how to use component-base/logs and what effect the different command line options have. Like most Kubernetes components, `cmd` uses Cobra and pflags. `stdlib` uses just plain Go libraries. `test` contains a unit test with per-test output. `slog2k8s` shows how an application using `log/slog` from Go 1.21 can include packages from Kubernetes. `k8s2slog` is the other direction. Below we can see examples of how some features work. ## Default Run: ```console go run ./staging/src/k8s.io/component-base/logs/example/cmd/logger.go ``` Expected output: ``` I0329 11:36:38.734334 99095 logger.go:44] "Oops, I shouldn't be logging yet!" This is normal output via stdout. This is other output via stderr. I0329 11:36:38.734575 99095 logger.go:76] Log using Infof, key: value I0329 11:36:38.734592 99095 logger.go:77] "Log using InfoS" key="value" E0329 11:36:38.734604 99095 logger.go:79] Log using Errorf, err: fail E0329 11:36:38.734619 99095 logger.go:80] "Log using ErrorS" err="fail" I0329 11:36:38.734653 99095 logger.go:87] "Now the default logger is set, but using the one from the context is still better." I0329 11:36:38.734693 99095 logger.go:94] "runtime" duration="1m0s" I0329 11:36:38.734710 99095 logger.go:95] "another runtime" duration="1m0s" ``` ## JSON Run: ```console go run ./staging/src/k8s.io/component-base/logs/example/cmd/logger.go --logging-format json ``` Expected output: ``` I0329 11:38:01.782592 99945 logger.go:44] "Oops, I shouldn't be logging yet!" This is normal output via stdout. This is other output via stderr. {"ts":1648546681782.9036,"caller":"cmd/logger.go:76","msg":"Log using Infof, key: value\n","v":0} {"ts":1648546681782.9392,"caller":"cmd/logger.go:77","msg":"Log using InfoS","v":0,"key":"value"} {"ts":1648546681782.9763,"caller":"cmd/logger.go:79","msg":"Log using Errorf, err: fail\n"} {"ts":1648546681782.9915,"caller":"cmd/logger.go:80","msg":"Log using ErrorS","err":"fail"} {"ts":1648546681783.0364,"caller":"cmd/logger.go:87","msg":"Now the default logger is set, but using the one from the context is still better.","v":0} {"ts":1648546681783.1091,"caller":"cmd/logger.go:94","msg":"runtime","v":0,"duration":"1m0s"} {"ts":1648546681783.1257,"caller":"cmd/logger.go:95","msg":"another runtime","v":0,"duration":"1h0m0s","duration":"1m0s"} ``` ## Verbosity ```console go run ./staging/src/k8s.io/component-base/logs/example/cmd/logger.go -v1 ``` The expected output now includes `Log less important message`: ``` I0329 11:38:23.145695 100190 logger.go:44] "Oops, I shouldn't be logging yet!" This is normal output via stdout. This is other output via stderr. I0329 11:38:23.145944 100190 logger.go:76] Log using Infof, key: value I0329 11:38:23.145961 100190 logger.go:77] "Log using InfoS" key="value" E0329 11:38:23.145973 100190 logger.go:79] Log using Errorf, err: fail E0329 11:38:23.145989 100190 logger.go:80] "Log using ErrorS" err="fail" I0329 11:38:23.146017 100190 logger.go:83] Log less important message I0329 11:38:23.146034 100190 logger.go:87] "Now the default logger is set, but using the one from the context is still better." I0329 11:38:23.146074 100190 logger.go:94] "runtime" duration="1m0s" I0329 11:38:23.146091 100190 logger.go:95] "another runtime" duration="1m0s" ``` ## Contextual logging Contextual logging enables the caller of the function to add a string prefix and additional key/value pairs to a logger and then pass the updated logger into functions via a `context` parameter. At the moment, this functionality is controlled in Kubernetes with the `ContextualLogging` feature gate and disabled by default. `klog.LoggerWithValues`, `klog.LoggerWithName`, `klog.NewContext` just return the original instance when contextual logging is disabled. `klog.FromContext` doesn't check the context for a logger and instead returns the global logger. ```console go run ./staging/src/k8s.io/component-base/logs/example/cmd/logger.go --feature-gates ContextualLogging=true ``` The expected output now includes `example` (added by caller) and `myname` (added by callee) as prefix and the caller's `foo="bar"` key/value pair: ``` I0329 11:47:36.830458 101057 logger.go:44] "Oops, I shouldn't be logging yet!" This is normal output via stdout. This is other output via stderr. I0329 11:47:36.830715 101057 logger.go:76] Log using Infof, key: value I0329 11:47:36.830731 101057 logger.go:77] "Log using InfoS" key="value" E0329 11:47:36.830745 101057 logger.go:79] Log using Errorf, err: fail E0329 11:47:36.830760 101057 logger.go:80] "Log using ErrorS" err="fail" I0329 11:47:36.830795 101057 logger.go:87] "Now the default logger is set, but using the one from the context is still better." I0329 11:47:36.830841 101057 logger.go:94] "example/myname: runtime" foo="bar" duration="1m0s" I0329 11:47:36.830859 101057 logger.go:95] "example: another runtime" foo="bar" duration="1m0s" ``` kubernetes-component-base-1b2882b/logs/example/cmd/000077500000000000000000000000001476422213000222665ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/example/cmd/logger.go000066400000000000000000000036521476422213000241020ustar00rootroot00000000000000/* Copyright 2018 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 main import ( "context" "fmt" "os" "strings" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/component-base/cli" "k8s.io/component-base/featuregate" "k8s.io/component-base/logs" logsapi "k8s.io/component-base/logs/api/v1" "k8s.io/component-base/logs/example" "k8s.io/klog/v2" _ "k8s.io/component-base/logs/json/register" ) var featureGate = featuregate.NewFeatureGate() func main() { runtime.Must(logsapi.AddFeatureGates(featureGate)) command := NewLoggerCommand() code := cli.Run(command) os.Exit(code) } func NewLoggerCommand() *cobra.Command { c := logsapi.NewLoggingConfiguration() cmd := &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { logs.InitLogs() if err := logsapi.ValidateAndApply(c, featureGate); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } if len(args) > 0 { fmt.Fprintf(os.Stderr, "Unexpected additional command line arguments:\n %s\n", strings.Join(args, "\n ")) os.Exit(1) } // Initialize contextual logging. logger := klog.LoggerWithValues(klog.LoggerWithName(klog.Background(), "example"), "foo", "bar") ctx := klog.NewContext(context.Background(), logger) // Produce some output. example.Run(ctx) }, } logsapi.AddFeatureGates(featureGate) featureGate.AddFlag(cmd.Flags()) logsapi.AddFlags(c, cmd.Flags()) return cmd } kubernetes-component-base-1b2882b/logs/example/example.go000066400000000000000000000036331476422213000235120ustar00rootroot00000000000000/* Copyright 2023 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 example shows how a library uses contextual logging. package example import ( "context" "errors" "fmt" "os" "time" "k8s.io/klog/v2" ) func init() { // Intentionally broken: logging is not initialized yet. klog.TODO().Info("Oops, I shouldn't be logging yet!") } func Run(ctx context.Context) { fmt.Println("This is normal output via stdout.") fmt.Fprintln(os.Stderr, "This is other output via stderr.") klog.Infof("Log using Infof, key: %s", "value") klog.InfoS("Log using InfoS", "key", "value") err := errors.New("fail") klog.Errorf("Log using Errorf, err: %v", err) klog.ErrorS(err, "Log using ErrorS") klog.V(1).Info("Log less important message") // This is the fallback that can be used if neither logger nor context // are available... but it's better to pass some kind of parameter. klog.TODO().Info("Now the default logger is set, but using the one from the context is still better.") logger := klog.FromContext(ctx) logger.V(5).Info("Log less important message at V=5 through context") // This intentionally uses the same key/value multiple times. Only the // second example could be detected via static code analysis. klog.LoggerWithValues(klog.LoggerWithName(logger, "myname"), "duration", time.Hour).Info("runtime", "duration", time.Minute) logger.Info("another runtime", "duration", time.Hour, "duration", time.Minute) } kubernetes-component-base-1b2882b/logs/example/k8s2slog/000077500000000000000000000000001476422213000231775ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/example/k8s2slog/k8s2slog.go000066400000000000000000000050651476422213000252100ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2018 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 main import ( "context" "fmt" "log/slog" "os" "strings" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/component-base/cli" "k8s.io/component-base/featuregate" "k8s.io/component-base/logs" logsapi "k8s.io/component-base/logs/api/v1" "k8s.io/klog/v2" _ "k8s.io/component-base/logs/json/register" ) var featureGate = featuregate.NewFeatureGate() func main() { runtime.Must(logsapi.AddFeatureGates(featureGate)) command := NewLoggerCommand() code := cli.Run(command) os.Exit(code) } func NewLoggerCommand() *cobra.Command { c := logsapi.NewLoggingConfiguration() cmd := &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { // This configures the global logger in klog *and* slog, if compiled // with Go >= 1.21. logs.InitLogs() if err := logsapi.ValidateAndApply(c, featureGate); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } if len(args) > 0 { fmt.Fprintf(os.Stderr, "Unexpected additional command line arguments:\n %s\n", strings.Join(args, "\n ")) os.Exit(1) } // Produce some output. Special types used by Kubernetes work. podRef := klog.KObj(&metav1.ObjectMeta{Name: "some-pod", Namespace: "some-namespace"}) podRefs := klog.KObjSlice([]interface{}{ &metav1.ObjectMeta{Name: "some-pod", Namespace: "some-namespace"}, nil, &metav1.ObjectMeta{Name: "other-pod"}, }) slog.Info("slog.Info", "pod", podRef, "pods", podRefs) klog.InfoS("klog.InfoS", "pod", podRef, "pods", podRefs) klog.Background().Info("klog.Background+logr.Logger.Info") klog.FromContext(context.Background()).Info("klog.FromContext+logr.Logger.Info") slogLogger := slog.Default() slogLogger.Info("slog.Default+slog.Logger.Info") }, } if err := logsapi.AddFeatureGates(featureGate); err != nil { // Shouldn't happen. panic(err) } featureGate.AddFlag(cmd.Flags()) logsapi.AddFlags(c, cmd.Flags()) return cmd } kubernetes-component-base-1b2882b/logs/example/slog2k8s/000077500000000000000000000000001476422213000231775ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/example/slog2k8s/slog2k8s.go000066400000000000000000000030031476422213000251760ustar00rootroot00000000000000//go:build go1.21 // +build go1.21 /* Copyright 2023 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. */ // slog2k8s demonstrates how an application using log/slog for logging // can include Kubernetes packages. package main import ( "context" "log/slog" "os" "k8s.io/klog/v2" ) func main() { options := slog.HandlerOptions{AddSource: true} textHandler := slog.NewTextHandler(os.Stderr, &options) textLogger := slog.New(textHandler) // Use text output as default logger. slog.SetDefault(textLogger) // This also needs to be done through klog to ensure that all code // using klog uses the text handler. klog.Background/TODO/FromContext // will return a thin wrapper around the textHandler, so all that klog // still does is manage the global default and retrieval from contexts. klog.SetSlogLogger(textLogger) textLogger.Info("slog.Logger.Info") klog.InfoS("klog.InfoS") klog.Background().Info("klog.Background+logr.Logger.Info") klog.FromContext(context.Background()).Info("klog.FromContext+logr.Logger.Info") } kubernetes-component-base-1b2882b/logs/example/stdlib/000077500000000000000000000000001476422213000230045ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/example/stdlib/logger.go000066400000000000000000000041041476422213000246110ustar00rootroot00000000000000/* Copyright 2023 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 main import ( "context" "flag" "fmt" "os" "strings" "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/component-base/featuregate" "k8s.io/component-base/logs" logsapi "k8s.io/component-base/logs/api/v1" "k8s.io/component-base/logs/example" "k8s.io/klog/v2" _ "k8s.io/component-base/logs/json/register" ) func main() { // Set up command line, including a feature gate parameter and logging options. featureGate := featuregate.NewFeatureGate() runtime.Must(logsapi.AddFeatureGates(featureGate)) flag.Var(featureGate, "feature-gate", "A set of key=value pairs that describe feature gates for alpha/experimental features. "+ "Options are:\n"+strings.Join(featureGate.KnownFeatures(), "\n")) c := logsapi.NewLoggingConfiguration() logsapi.AddGoFlags(c, flag.CommandLine) // Parse flags and apply the result. logs.InitLogs disables contextual // logging while it is still alpha. The feature gate parameter must be // used to enable it explicitly. flag.Parse() logs.InitLogs() if err := logsapi.ValidateAndApply(c, featureGate); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } args := flag.CommandLine.Args() if len(args) > 0 { fmt.Fprintf(os.Stderr, "Unexpected additional command line arguments:\n %s\n", strings.Join(args, "\n ")) os.Exit(1) } // Initialize contextual logging. logger := klog.LoggerWithValues(klog.LoggerWithName(klog.Background(), "example"), "foo", "bar") ctx := klog.NewContext(context.Background(), logger) // Produce some output. example.Run(ctx) } kubernetes-component-base-1b2882b/logs/example/test/000077500000000000000000000000001476422213000225025ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/example/test/logger_test.go000066400000000000000000000033141476422213000253500ustar00rootroot00000000000000/* Copyright 2023 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 logger_test demonstrates how to write unit tests with per-test // output. Per-test output only works for code which supports contextual // logging. package logger_test import ( "flag" "os" "testing" "k8s.io/component-base/logs/example" "k8s.io/klog/v2/ktesting" // This import could be used to add command line flags for per-test // output with a default verbosity of 5. This example instead // uses a TestMain where the default verbosity gets lowered. // "k8s.io/klog/v2/ktesting/init" ) func TestLogger(t *testing.T) { // Produce some output in two different tests which run in parallel. for _, testcase := range []string{"abc", "xyz"} { t.Run(testcase, func(t *testing.T) { t.Parallel() _ /* logger */, ctx := ktesting.NewTestContext(t) example.Run(ctx) }) } } func TestMain(m *testing.M) { // Run with verbosity 2, this is the default log level in production. // The name of the flags also could be customized here. The defaults // are -testing.v and -testing.vmodule. ktesting.DefaultConfig = ktesting.NewConfig(ktesting.Verbosity(2)) ktesting.DefaultConfig.AddFlags(flag.CommandLine) flag.Parse() os.Exit(m.Run()) } kubernetes-component-base-1b2882b/logs/internal/000077500000000000000000000000001476422213000217045ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/internal/setverbositylevel/000077500000000000000000000000001476422213000254765ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/internal/setverbositylevel/setverbositylevel.go000066400000000000000000000020041476422213000316130ustar00rootroot00000000000000/* Copyright 2022 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 setverbositylevel stores callbacks that will be invoked by logs.GlogLevel. // // This is a separate package to avoid a dependency from // k8s.io/component-base/logs (uses the callbacks) to // k8s.io/component-base/logs/api/v1 (adds them). Not all users of the logs // package also use the API. package setverbositylevel import ( "sync" ) var ( // Mutex controls access to the callbacks. Mutex sync.Mutex Callbacks []func(v uint32) error ) kubernetes-component-base-1b2882b/logs/json/000077500000000000000000000000001476422213000210415ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/json/json.go000066400000000000000000000110771476422213000223470ustar00rootroot00000000000000/* Copyright 2020 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 json import ( "io" "sync/atomic" "time" "github.com/go-logr/logr" "github.com/go-logr/zapr" "go.uber.org/zap" "go.uber.org/zap/zapcore" "k8s.io/component-base/featuregate" logsapi "k8s.io/component-base/logs/api/v1" ) var ( // timeNow stubbed out for testing timeNow = time.Now ) type runtime struct { v uint32 } func (r *runtime) ZapV() zapcore.Level { // zap levels are inverted: everything with a verbosity >= threshold gets logged. return -zapcore.Level(atomic.LoadUint32(&r.v)) } // Enabled implements the zapcore.LevelEnabler interface. func (r *runtime) Enabled(level zapcore.Level) bool { return level >= r.ZapV() } func (r *runtime) SetVerbosityLevel(v uint32) error { atomic.StoreUint32(&r.v, v) return nil } var _ zapcore.LevelEnabler = &runtime{} // NewJSONLogger creates a new json logr.Logger and its associated // control interface. The separate error stream is optional and may be nil. // The encoder config is also optional. func NewJSONLogger(v logsapi.VerbosityLevel, infoStream, errorStream zapcore.WriteSyncer, encoderConfig *zapcore.EncoderConfig) (logr.Logger, logsapi.RuntimeControl) { r := &runtime{v: uint32(v)} if encoderConfig == nil { encoderConfig = &zapcore.EncoderConfig{ MessageKey: "msg", CallerKey: "caller", NameKey: "logger", TimeKey: "ts", EncodeTime: epochMillisTimeEncoder, EncodeDuration: zapcore.StringDurationEncoder, EncodeCaller: zapcore.ShortCallerEncoder, } } encoder := zapcore.NewJSONEncoder(*encoderConfig) var core zapcore.Core if errorStream == nil { core = zapcore.NewCore(encoder, infoStream, r) } else { highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= zapcore.ErrorLevel && r.Enabled(lvl) }) lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl < zapcore.ErrorLevel && r.Enabled(lvl) }) core = zapcore.NewTee( zapcore.NewCore(encoder, errorStream, highPriority), zapcore.NewCore(encoder, infoStream, lowPriority), ) } l := zap.New(core, zap.WithCaller(true)) return zapr.NewLoggerWithOptions(l, zapr.LogInfoLevel("v"), zapr.ErrorKey("err")), logsapi.RuntimeControl{ SetVerbosityLevel: r.SetVerbosityLevel, Flush: func() { _ = l.Sync() }, } } func epochMillisTimeEncoder(_ time.Time, enc zapcore.PrimitiveArrayEncoder) { nanos := timeNow().UnixNano() millis := float64(nanos) / float64(time.Millisecond) enc.AppendFloat64(millis) } // Factory produces JSON logger instances. type Factory struct{} var _ logsapi.LogFormatFactory = Factory{} func (f Factory) Feature() featuregate.Feature { return logsapi.LoggingBetaOptions } func (f Factory) Create(c logsapi.LoggingConfiguration, o logsapi.LoggingOptions) (logr.Logger, logsapi.RuntimeControl) { // We intentionally avoid all os.File.Sync calls. Output is unbuffered, // therefore we don't need to flush, and calling the underlying fsync // would just slow down writing. // // The assumption is that logging only needs to ensure that data gets // written to the output stream before the process terminates, but // doesn't need to worry about data not being written because of a // system crash or powerloss. stderr := zapcore.Lock(AddNopSync(o.ErrorStream)) if c.Options.JSON.SplitStream { stdout := zapcore.Lock(AddNopSync(o.InfoStream)) size := c.Options.JSON.InfoBufferSize.Value() if size > 0 { // Prevent integer overflow. if size > 2*1024*1024*1024 { size = 2 * 1024 * 1024 * 1024 } stdout = &zapcore.BufferedWriteSyncer{ WS: stdout, Size: int(size), } } // stdout for info messages, stderr for errors. return NewJSONLogger(c.Verbosity, stdout, stderr, nil) } // Write info messages and errors to stderr to prevent mixing with normal program output. return NewJSONLogger(c.Verbosity, stderr, nil, nil) } // AddNoSync adds a NOP Sync implementation. func AddNopSync(writer io.Writer) zapcore.WriteSyncer { return nopSync{Writer: writer} } type nopSync struct { io.Writer } func (f nopSync) Sync() error { return nil } kubernetes-component-base-1b2882b/logs/json/json_benchmark_test.go000066400000000000000000000052721476422213000254200ustar00rootroot00000000000000/* Copyright 2020 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 json import ( "fmt" "testing" "go.uber.org/zap/zapcore" ) var writer = zapcore.AddSync(&writeSyncer{}) func BenchmarkInfoLoggerInfo(b *testing.B) { logger, _ := NewJSONLogger(0, writer, nil, nil) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Info("test", "str", "foo", "int64A", int64(1), "int64B", int64(1), "float64", float64(1.0), "string1", "\n", "string2", "💩", "string3", "🤔", "string4", "🙊", "bool", true, "request", struct { Method string `json:"method"` Timeout int `json:"timeout"` secret string }{ Method: "GET", Timeout: 10, secret: "pony", }, ) } }) } func BenchmarkZapLoggerError(b *testing.B) { logger, _ := NewJSONLogger(0, writer, nil, nil) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.Error(fmt.Errorf("test for error:%s", "default"), "test", "str", "foo", "int64A", int64(1), "int64B", int64(1), "float64", float64(1.0), "string1", "\n", "string2", "💩", "string3", "🤔", "string4", "🙊", "bool", true, "request", struct { Method string `json:"method"` Timeout int `json:"timeout"` secret string }{ Method: "GET", Timeout: 10, secret: "pony", }, ) } }) } func BenchmarkZapLoggerV(b *testing.B) { logger, _ := NewJSONLogger(1, writer, nil, nil) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { logger.V(1).Info("test", "str", "foo", "int64A", int64(1), "int64B", int64(1), "float64", float64(1.0), "string1", "\n", "string2", "💩", "string3", "🤔", "string4", "🙊", "bool", true, "request", struct { Method string `json:"method"` Timeout int `json:"timeout"` secret string }{ Method: "GET", Timeout: 10, secret: "pony", }, ) } }) } type writeSyncer struct{} var _ zapcore.WriteSyncer = (*writeSyncer)(nil) func (w writeSyncer) Write(p []byte) (n int, err error) { return len(p), nil } func (w writeSyncer) Sync() error { return nil } kubernetes-component-base-1b2882b/logs/json/json_test.go000066400000000000000000000225251476422213000234060ustar00rootroot00000000000000/* Copyright 2020 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 json import ( "bytes" "fmt" "strings" "testing" "time" "github.com/stretchr/testify/assert" "go.uber.org/zap" "go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/types" logsapi "k8s.io/component-base/logs/api/v1" ) // TestZapLoggerInfo test ZapLogger json info format func TestZapLoggerInfo(t *testing.T) { timeNow = func() time.Time { return time.Date(1970, time.January, 1, 0, 0, 0, 123, time.UTC) } var testDataInfo = []struct { msg string format string keysValues []interface{} names []string }{ { msg: "test", format: "{\"ts\":%f,\"caller\":\"json/json_test.go:%d\",\"msg\":\"test\",\"v\":0,\"ns\":\"default\",\"podnum\":2}\n", keysValues: []interface{}{"ns", "default", "podnum", 2}, }, { msg: "test for strongly typed Zap field", format: "{\"ts\":%f,\"caller\":\"json/json_test.go:%d\",\"msg\":\"strongly-typed Zap Field passed to logr\",\"zap field\":{\"Key\":\"attempt\",\"Type\":11,\"Integer\":3,\"String\":\"\",\"Interface\":null}}\n{\"ts\":%f,\"caller\":\"json/json_test.go:%d\",\"msg\":\"test for strongly typed Zap field\",\"v\":0,\"ns\":\"default\",\"podnum\":2}\n", keysValues: []interface{}{"ns", "default", "podnum", 2, zap.Int("attempt", 3), "attempt", "Running", 10}, }, { msg: "test for non-string key argument", format: "{\"ts\":%f,\"caller\":\"json/json_test.go:%d\",\"msg\":\"non-string key argument passed to logging, ignoring all later arguments\",\"invalid key\":200}\n{\"ts\":%f,\"caller\":\"json/json_test.go:%d\",\"msg\":\"test for non-string key argument\",\"v\":0,\"ns\":\"default\",\"podnum\":2}\n", keysValues: []interface{}{"ns", "default", "podnum", 2, 200, "replica", "Running", 10}, }, { msg: "test for duration value argument", format: "{\"ts\":%f,\"caller\":\"json/json_test.go:%d\",\"msg\":\"test for duration value argument\",\"v\":0,\"duration\":\"5s\"}\n", keysValues: []interface{}{"duration", time.Duration(5 * time.Second)}, }, { msg: "test for WithName", names: []string{"hello", "world"}, format: "{\"ts\":%f,\"logger\":\"hello.world\",\"caller\":\"json/json_test.go:%d\",\"msg\":\"test for WithName\",\"v\":0}\n", }, { msg: "test for duplicate keys", format: "{\"ts\":%f,\"caller\":\"json/json_test.go:%d\",\"msg\":\"test for duplicate keys\",\"v\":0,\"akey\":\"avalue\",\"akey\":\"anothervalue\"}\n", keysValues: []interface{}{"akey", "avalue", "akey", "anothervalue"}, }, { msg: "test for NamespacedName argument", format: "{\"ts\":%f,\"caller\":\"json/json_test.go:%d\",\"msg\":\"test for NamespacedName argument\",\"v\":0,\"obj\":{\"name\":\"kube-proxy\",\"namespace\":\"kube-system\"}}\n", keysValues: []interface{}{"obj", types.NamespacedName{Name: "kube-proxy", Namespace: "kube-system"}}, }, { msg: "test for NamespacedName argument with no namespace", format: "{\"ts\":%f,\"caller\":\"json/json_test.go:%d\",\"msg\":\"test for NamespacedName argument with no namespace\",\"v\":0,\"obj\":{\"name\":\"kube-proxy\"}}\n", keysValues: []interface{}{"obj", types.NamespacedName{Name: "kube-proxy"}}, }, } for _, data := range testDataInfo { var buffer bytes.Buffer writer := zapcore.AddSync(&buffer) sampleInfoLogger, _ := NewJSONLogger(0, writer, nil, nil) for _, name := range data.names { // nolint:logcheck // This intentionally ignore the feature gate and always tests with a name. sampleInfoLogger = sampleInfoLogger.WithName(name) } // nolint:logcheck // The linter cannot and doesn't need to check the key/value pairs. sampleInfoLogger.Info(data.msg, data.keysValues...) logStr := buffer.String() logStrLines := strings.Split(logStr, "\n") dataFormatLines := strings.Split(data.format, "\n") if !assert.Equal(t, len(logStrLines), len(dataFormatLines)) { t.Errorf("Info has wrong format: no. of lines in log is incorrect \n expect:%d\n got:%d", len(dataFormatLines), len(logStrLines)) } for i := range logStrLines { if len(logStrLines[i]) == 0 && len(dataFormatLines[i]) == 0 { continue } var ts float64 var lineNo int n, err := fmt.Sscanf(logStrLines[i], dataFormatLines[i], &ts, &lineNo) if n != 2 || err != nil { t.Errorf("log format error: %d elements, error %s:\n%s", n, err, logStrLines[i]) } expect := fmt.Sprintf(dataFormatLines[i], ts, lineNo) if !assert.Equal(t, expect, logStrLines[i]) { t.Errorf("Info has wrong format \n expect:%s\n got:%s", expect, logStrLines[i]) } } } } // TestZapLoggerEnabled test ZapLogger enabled func TestZapLoggerEnabled(t *testing.T) { verbosityLevel := 10 sampleInfoLogger, _ := NewJSONLogger(logsapi.VerbosityLevel(verbosityLevel), nil, nil, nil) for v := 0; v <= verbosityLevel; v++ { enabled := sampleInfoLogger.V(v).Enabled() expectEnabled := v <= verbosityLevel if !expectEnabled && enabled { t.Errorf("V(%d).Info should be disabled", v) } if expectEnabled && !enabled { t.Errorf("V(%d).Info should be enabled", v) } } } // TestZapLoggerV test ZapLogger V set log level func func TestZapLoggerV(t *testing.T) { timeNow = func() time.Time { return time.Date(1970, time.January, 1, 0, 0, 0, 123, time.UTC) } verbosityLevel := 10 for v := 0; v <= verbosityLevel; v++ { var buffer bytes.Buffer writer := zapcore.AddSync(&buffer) sampleInfoLogger, _ := NewJSONLogger(logsapi.VerbosityLevel(verbosityLevel), writer, nil, nil) sampleInfoLogger.V(v).Info("test", "ns", "default", "podnum", 2, "time", time.Microsecond) logStr := buffer.String() shouldHaveLogged := v <= verbosityLevel if logged := logStr != ""; logged != shouldHaveLogged { if logged { t.Fatalf("Expected no output at v=%d, got: %s", v, logStr) } t.Fatalf("Expected output at v=%d, got none.", v) } if !shouldHaveLogged { continue } var actualV, lineNo int expectFormat := "{\"ts\":0.000123,\"caller\":\"json/json_test.go:%d\",\"msg\":\"test\",\"v\":%d,\"ns\":\"default\",\"podnum\":2,\"time\":\"1µs\"}\n" n, err := fmt.Sscanf(logStr, expectFormat, &lineNo, &actualV) if n != 2 || err != nil { t.Errorf("log format error: %d elements, error %s:\n%s", n, err, logStr) } if actualV != v { t.Errorf("V(%d).Info...) returned v=%d. expected v=%d", v, actualV, v) } expect := fmt.Sprintf(expectFormat, lineNo, v) if !assert.Equal(t, expect, logStr) { t.Errorf("V(%d).Info has wrong format \n expect:%s\n got:%s", v, expect, logStr) } buffer.Reset() } } // TestZapLoggerError test ZapLogger json error format func TestZapLoggerError(t *testing.T) { var buffer bytes.Buffer writer := zapcore.AddSync(&buffer) timeNow = func() time.Time { return time.Date(1970, time.January, 1, 0, 0, 0, 123, time.UTC) } sampleInfoLogger, _ := NewJSONLogger(0, writer, nil, nil) sampleInfoLogger.Error(fmt.Errorf("invalid namespace:%s", "default"), "wrong namespace", "ns", "default", "podnum", 2, "time", time.Microsecond) logStr := buffer.String() var ts float64 var lineNo int expectFormat := `{"ts":%f,"caller":"json/json_test.go:%d","msg":"wrong namespace","ns":"default","podnum":2,"time":"1µs","err":"invalid namespace:default"}` n, err := fmt.Sscanf(logStr, expectFormat, &ts, &lineNo) if n != 2 || err != nil { t.Errorf("log format error: %d elements, error %s:\n%s", n, err, logStr) } expect := fmt.Sprintf(expectFormat, ts, lineNo) if !assert.JSONEq(t, expect, logStr) { t.Errorf("Info has wrong format \n expect:%s\n got:%s", expect, logStr) } } func TestZapLoggerStreams(t *testing.T) { var infoBuffer, errorBuffer bytes.Buffer log, _ := NewJSONLogger(0, zapcore.AddSync(&infoBuffer), zapcore.AddSync(&errorBuffer), nil) log.Error(fmt.Errorf("some error"), "failed") log.Info("hello world") logStr := errorBuffer.String() var ts float64 var lineNo int expectFormat := `{"ts":%f,"caller":"json/json_test.go:%d","msg":"failed","err":"some error"}` n, err := fmt.Sscanf(logStr, expectFormat, &ts, &lineNo) if n != 2 || err != nil { t.Errorf("error log format error: %d elements, error %s:\n%s", n, err, logStr) } expect := fmt.Sprintf(expectFormat, ts, lineNo) if !assert.JSONEq(t, expect, logStr) { t.Errorf("error log has wrong format \n expect:%s\n got:%s", expect, logStr) } logStr = infoBuffer.String() expectFormat = `{"ts":%f,"caller":"json/json_test.go:%d","msg":"hello world","v":0}` n, err = fmt.Sscanf(logStr, expectFormat, &ts, &lineNo) if n != 2 || err != nil { t.Errorf("info log format error: %d elements, error %s:\n%s", n, err, logStr) } expect = fmt.Sprintf(expectFormat, ts, lineNo) if !assert.JSONEq(t, expect, logStr) { t.Errorf("info has wrong format \n expect:%s\n got:%s", expect, logStr) } } type testBuff struct { writeCount int } // Sync syncs data to file func (b *testBuff) Sync() error { return nil } // Write writes data to buffer func (b *testBuff) Write(p []byte) (int, error) { b.writeCount++ return len(p), nil } kubernetes-component-base-1b2882b/logs/json/klog_test.go000066400000000000000000000167261476422213000233770ustar00rootroot00000000000000/* Copyright 2020 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 json import ( "bytes" "errors" "flag" "fmt" "strings" "testing" "time" "github.com/stretchr/testify/assert" "go.uber.org/zap/zapcore" logsapi "k8s.io/component-base/logs/api/v1" "k8s.io/klog/v2" ) type kmeta struct { Name, Namespace string } func (k kmeta) GetName() string { return k.Name } func (k kmeta) GetNamespace() string { return k.Namespace } var _ klog.KMetadata = kmeta{} func TestKlogIntegration(t *testing.T) { timeNow = func() time.Time { return time.Date(1970, time.January, 1, 0, 0, 0, 123, time.UTC) } fs := flag.FlagSet{} klog.InitFlags(&fs) err := fs.Set("v", "2") if err != nil { t.Fatalf("Failed to set verbosity") } tcs := []struct { name string fun func() format string }{ { name: "Info", fun: func() { klog.Info("test ", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1","v":0}`, }, { name: "V(1).Info", fun: func() { klog.V(1).Info("test ", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1","v":1}`, }, { name: "Infof", fun: func() { klog.Infof("test %d", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1","v":0}`, }, { name: "V(1).Infof", fun: func() { klog.V(1).Infof("test %d", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1","v":1}`, }, { name: "Infoln", fun: func() { klog.Infoln("test", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1","v":0}`, }, { name: "V(1).Infoln", fun: func() { klog.V(1).Infoln("test", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1","v":1}`, }, { name: "InfoDepth", fun: func() { klog.InfoDepth(1, "test ", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1","v":0}`, }, { name: "InfoS", fun: func() { klog.InfoS("test", "count", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test","v":0,"count":1}`, }, { name: "V(1).InfoS", fun: func() { klog.V(1).InfoS("test", "count", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test","v":1,"count":1}`, }, { name: "V(2).InfoS", fun: func() { klog.V(2).InfoS("test", "count", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test","v":2,"count":1}`, }, { name: "V(3).InfoS", fun: func() { klog.V(3).InfoS("test", "count", 1) }, // no output because of threshold 2 }, { name: "InfoSDepth", fun: func() { klog.InfoSDepth(1, "test", "count", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test","v":0,"count":1}`, }, { name: "KObj", fun: func() { klog.InfoS("some", "pod", klog.KObj(&kmeta{Name: "pod-1", Namespace: "kube-system"})) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"some","v":0,"pod":{"name":"pod-1","namespace":"kube-system"}}`, }, { name: "KObjSlice", fun: func() { klog.InfoS("several", "pods", klog.KObjSlice([]interface{}{ &kmeta{Name: "pod-1", Namespace: "kube-system"}, &kmeta{Name: "pod-2", Namespace: "kube-system"}, })) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"several","v":0,"pods":[{"name":"pod-1","namespace":"kube-system"},{"name":"pod-2","namespace":"kube-system"}]}`, }, { name: "Warning", fun: func() { klog.Warning("test ", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1","v":0}`, }, { name: "WarningDepth", fun: func() { klog.WarningDepth(1, "test ", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1","v":0}`, }, { name: "Warningln", fun: func() { klog.Warningln("test", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1","v":0}`, }, { name: "Warningf", fun: func() { klog.Warningf("test %d", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1","v":0}`, }, { name: "Error", fun: func() { klog.Error("test ", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1"}`, }, { name: "ErrorDepth", fun: func() { klog.ErrorDepth(1, "test ", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1"}`, }, { name: "Errorln", fun: func() { klog.Errorln("test", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1"}`, }, { name: "Errorf", fun: func() { klog.Errorf("test %d", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test 1"}`, }, { name: "ErrorS", fun: func() { err := errors.New("fail") klog.ErrorS(err, "test", "count", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test","count":1,"err":"fail"}`, }, { name: "ErrorSDepth", fun: func() { err := errors.New("fail") klog.ErrorSDepth(1, err, "test", "count", 1) }, format: `{"ts":%f,"caller":"json/klog_test.go:%d","msg":"test","count":1,"err":"fail"}`, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { var buffer bytes.Buffer writer := zapcore.AddSync(&buffer) // This level is high enough to enable all log messages from this test. verbosityLevel := logsapi.VerbosityLevel(100) logger, _ := NewJSONLogger(verbosityLevel, writer, nil, nil) klog.SetLogger(logger) defer klog.ClearLogger() tc.fun() var ts float64 var lineNo int logString := strings.TrimSuffix(buffer.String(), "\n") if tc.format == "" { if logString != "" { t.Fatalf("expected no output, got: %s", logString) } return } n, err := fmt.Sscanf(logString, tc.format, &ts, &lineNo) if n != 2 || err != nil { t.Errorf("log format error: %d elements, error %s:\n%s", n, err, logString) } expect := fmt.Sprintf(tc.format, ts, lineNo) if !assert.Equal(t, expect, logString) { t.Errorf("Info has wrong format \n expect:%s\n got:%s", expect, logString) } }) } } // TestKlogV test klog -v(--verbose) func available with json logger func TestKlogV(t *testing.T) { var buffer testBuff writer := zapcore.AddSync(&buffer) logger, _ := NewJSONLogger(100, writer, nil, nil) klog.SetLogger(logger) defer klog.ClearLogger() fs := flag.FlagSet{} klog.InitFlags(&fs) totalLogsWritten := 0 defer func() { err := fs.Set("v", "0") if err != nil { t.Fatalf("Failed to reset verbosity to 0") } }() for i := 0; i < 11; i++ { err := fs.Set("v", fmt.Sprintf("%d", i)) if err != nil { t.Fatalf("Failed to set verbosity") } for j := 0; j < 11; j++ { klog.V(klog.Level(j)).Info("test", "time", time.Microsecond) logWritten := buffer.writeCount > 0 totalLogsWritten += buffer.writeCount buffer.writeCount = 0 if logWritten == (i < j) { t.Errorf("klog.V(%d).Info(...) wrote log when -v=%d", j, i) } } } if totalLogsWritten != 66 { t.Fatalf("Unexpected number of logs written, got %d, expected 66", totalLogsWritten) } } kubernetes-component-base-1b2882b/logs/json/register/000077500000000000000000000000001476422213000226655ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/json/register/register.go000066400000000000000000000015541476422213000250450ustar00rootroot00000000000000/* Copyright 2021 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 register import ( logsapi "k8s.io/component-base/logs/api/v1" json "k8s.io/component-base/logs/json" ) func init() { // JSON format is optional klog format if err := logsapi.RegisterLogFormat(logsapi.JSONLogFormat, json.Factory{}, logsapi.LoggingBetaOptions); err != nil { panic(err) } } kubernetes-component-base-1b2882b/logs/json/register/register_test.go000066400000000000000000000124471476422213000261070ustar00rootroot00000000000000/* Copyright 2021 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 register import ( "bytes" "testing" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/component-base/featuregate" logsapi "k8s.io/component-base/logs/api/v1" "k8s.io/klog/v2" ) func TestJSONFlag(t *testing.T) { c := logsapi.NewLoggingConfiguration() fs := pflag.NewFlagSet("addflagstest", pflag.ContinueOnError) output := bytes.Buffer{} logsapi.AddFlags(c, fs) fs.SetOutput(&output) fs.PrintDefaults() wantSubstring := `Permitted formats: "json" (gated by LoggingBetaOptions), "text".` if !assert.Contains(t, output.String(), wantSubstring) { t.Errorf("JSON logging format flag is not available. expect to contain %q, got %q", wantSubstring, output.String()) } } func TestJSONFormatRegister(t *testing.T) { config := logsapi.NewLoggingConfiguration() klogr := klog.Background() defaultGate := featuregate.NewFeatureGate() err := logsapi.AddFeatureGates(defaultGate) require.NoError(t, err) allEnabled := defaultGate.DeepCopy() allDisabled := defaultGate.DeepCopy() for feature := range defaultGate.GetAll() { if err := allEnabled.SetFromMap(map[string]bool{string(feature): true}); err != nil { panic(err) } if err := allDisabled.SetFromMap(map[string]bool{string(feature): false}); err != nil { panic(err) } } testcases := []struct { name string args []string contextualLogging bool featureGate featuregate.FeatureGate want *logsapi.LoggingConfiguration errs field.ErrorList }{ { name: "JSON log format, default gates", args: []string{"--logging-format=json"}, want: func() *logsapi.LoggingConfiguration { c := config.DeepCopy() c.Format = logsapi.JSONLogFormat return c }(), }, { name: "JSON log format, disabled gates", args: []string{"--logging-format=json"}, featureGate: allDisabled, want: func() *logsapi.LoggingConfiguration { c := config.DeepCopy() c.Format = logsapi.JSONLogFormat return c }(), errs: field.ErrorList{&field.Error{ Type: "FieldValueForbidden", Field: "format", BadValue: "", Detail: "Log format json is disabled, see LoggingBetaOptions feature", }}, }, { name: "JSON log format, enabled gates", args: []string{"--logging-format=json"}, featureGate: allEnabled, want: func() *logsapi.LoggingConfiguration { c := config.DeepCopy() c.Format = logsapi.JSONLogFormat return c }(), }, { name: "JSON log format", args: []string{"--logging-format=json"}, want: func() *logsapi.LoggingConfiguration { c := config.DeepCopy() c.Format = logsapi.JSONLogFormat return c }(), }, { name: "JSON direct", args: []string{"--logging-format=json"}, contextualLogging: true, want: func() *logsapi.LoggingConfiguration { c := config.DeepCopy() c.Format = logsapi.JSONLogFormat return c }(), }, { name: "Unsupported log format", args: []string{"--logging-format=test"}, want: func() *logsapi.LoggingConfiguration { c := config.DeepCopy() c.Format = "test" return c }(), errs: field.ErrorList{&field.Error{ Type: "FieldValueInvalid", Field: "format", BadValue: "test", Detail: "Unsupported log format", }}, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { state := klog.CaptureState() defer state.Restore() c := logsapi.NewLoggingConfiguration() fs := pflag.NewFlagSet("addflagstest", pflag.ContinueOnError) logsapi.AddFlags(c, fs) fs.Parse(tc.args) if !assert.Equal(t, tc.want, c) { t.Errorf("Wrong Validate() result for %q. expect %v, got %v", tc.name, tc.want, c) } featureGate := tc.featureGate if featureGate == nil { featureGate = defaultGate } mutable := featureGate.DeepCopy() err := mutable.SetFromMap(map[string]bool{string(logsapi.ContextualLogging): tc.contextualLogging}) require.NoError(t, err) featureGate = mutable defer func() { if err := logsapi.ResetForTest(featureGate); err != nil { t.Errorf("Unexpected error while resetting the logging configuration: %v", err) } }() errs := logsapi.ValidateAndApply(c, featureGate) if !assert.ElementsMatch(t, tc.errs, errs) { t.Errorf("Wrong Validate() result for %q.\n expect:\t%+v\n got:\t%+v", tc.name, tc.errs, errs) } currentLogger := klog.Background() isKlogr := currentLogger == klogr if tc.contextualLogging && isKlogr { t.Errorf("Expected to get zapr as logger, got: %T", currentLogger) } if !tc.contextualLogging && !isKlogr { t.Errorf("Expected to get klogr as logger, got: %T", currentLogger) } }) } } kubernetes-component-base-1b2882b/logs/klogflags/000077500000000000000000000000001476422213000220415ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/klogflags/klogflags.go000066400000000000000000000022311476422213000243370ustar00rootroot00000000000000/* Copyright 2022 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 klogflags import ( "flag" "k8s.io/klog/v2" ) // Init is a replacement for klog.InitFlags which only adds those flags // that are still supported for Kubernetes components (i.e. -v and -vmodule). // See // https://github.com/kubernetes/enhancements/tree/master/keps/sig-instrumentation/2845-deprecate-klog-specific-flags-in-k8s-components. func Init(fs *flag.FlagSet) { var allFlags flag.FlagSet klog.InitFlags(&allFlags) if fs == nil { fs = flag.CommandLine } allFlags.VisitAll(func(f *flag.Flag) { switch f.Name { case "v", "vmodule": fs.Var(f.Value, f.Name, f.Usage) } }) } kubernetes-component-base-1b2882b/logs/kube-log-runner/000077500000000000000000000000001476422213000231045ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/kube-log-runner/README.md000066400000000000000000000050121476422213000243610ustar00rootroot00000000000000# `kube-log-runner` (formerly known as go-runner) The `kube-log-runner` is a Go based binary that can run commands and redirect stdout/stderr etc. Why do we need this? - Some of our images like kube-apiserver used bash output redirection for collecting logs, so we were not able to switch to distroless images directly for these images. The klog's `--log-file` parameter was supposed to fix this problem, but we ran into trouble with that in scalability CI jobs that never could get root caused and fixed. Using this binary worked. - Windows services don't have a mechanism for redirecting output of a process. - Nowadays, the `--log-file` parameter is deprecated for Kubernetes components and should not be used anymore. `kube-log-runner` is a direct replacement. For example instead of running kube-apiserver like this: ```bash "/bin/sh", "-c", "exec kube-apiserver {{params}} --allow-privileged={{pillar['allow_privileged']}} 1>>/var/log/kube-apiserver.log 2>&1" ``` Or this: ```bash kube-apiserver {{params}} --allow-privileged={{pillar['allow_privileged']}} --log-file=/var/log/kube-apiserver.log --alsologtostderr=false" ``` We would use `kube-log-runner` like so: ```bash kube-log-runner -log-file=/var/log/kube-apiserver.log --also-stdout=false \ kube-apiserver {{params}} --allow-privileged={{pillar['allow_privileged']}} ``` The kube-log-runner then ensures that we run the `/usr/local/bin/kube-apiserver` with the specified parameters and redirect both stdout and stderr ONLY to the log file specified. It will always append to the log file. Possible invocations: ```bash # Merge stderr and stdout, write to stdout (same as 2>&1). kube-log-runner echo "hello world" # Redirect both into log file (same as 1>>/tmp/log 2>&1). kube-log-runner -log-file=/tmp/log echo "hello world" # Copy into log file and print to stdout (same as 2>&1 | tee -a /tmp/log). kube-log-runner -log-file=/tmp/log -also-stdout echo "hello world" # Redirect only stdout into log file (same as 1>>/tmp/log). kube-log-runner -log-file=/tmp/log -redirect-stderr=false echo "hello world" ``` # Container base image The Kubernetes [`registry.k8s.io/build-image/go-runner`](https://console.cloud.google.com/gcr/images/k8s-artifacts-prod/us/build-image/go-runner) image wraps the `gcr.io/distroless/static` image and provides `kube-log-runner` under its traditional name as `/go-runner`. It gets maintained in https://github.com/kubernetes/release/tree/master/images/build/go-runner. # Prebuilt binary The Kubernetes release archives contain kube-log-runner. kubernetes-component-base-1b2882b/logs/kube-log-runner/kube-log-runner.go000066400000000000000000000063621476422213000264560ustar00rootroot00000000000000/* Copyright 2020 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 main import ( "flag" "fmt" "io" "log" "os" "os/exec" "os/signal" "strings" "syscall" ) var ( logFilePath = flag.String("log-file", "", "If non-empty, save stdout to this file") alsoToStdOut = flag.Bool("also-stdout", false, "useful with log-file, log to standard output as well as the log file") redirectStderr = flag.Bool("redirect-stderr", true, "treat stderr same as stdout") ) func main() { flag.Parse() if err := configureAndRun(); err != nil { log.Fatal(err) } } func configureAndRun() error { var ( outputStream io.Writer = os.Stdout errStream io.Writer = os.Stderr ) args := flag.Args() if len(args) == 0 { return fmt.Errorf("not enough arguments to run") } if logFilePath != nil && *logFilePath != "" { logFile, err := os.OpenFile(*logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("failed to create log file %v: %w", *logFilePath, err) } if *alsoToStdOut { outputStream = io.MultiWriter(os.Stdout, logFile) } else { outputStream = logFile } } if *redirectStderr { errStream = outputStream } exe := args[0] var exeArgs []string if len(args) > 1 { exeArgs = args[1:] } cmd := exec.Command(exe, exeArgs...) cmd.Stdout = outputStream cmd.Stderr = errStream log.Printf("Running command:\n%v", cmdInfo(cmd)) err := cmd.Start() if err != nil { return fmt.Errorf("starting command: %w", err) } // Handle signals and shutdown process gracefully. go setupSigHandler(cmd.Process) if err := cmd.Wait(); err != nil { return fmt.Errorf("running command: %w", err) } return nil } // cmdInfo generates a useful look at what the command is for printing/debug. func cmdInfo(cmd *exec.Cmd) string { return fmt.Sprintf( `Command env: (log-file=%v, also-stdout=%v, redirect-stderr=%v) Run from directory: %v Executable path: %v Args (comma-delimited): %v`, *logFilePath, *alsoToStdOut, *redirectStderr, cmd.Dir, cmd.Path, strings.Join(cmd.Args, ","), ) } // setupSigHandler will forward any termination signals to the process func setupSigHandler(process *os.Process) { // terminationSignals are signals that cause the program to exit in the // supported platforms (linux, darwin, windows). terminationSignals := []os.Signal{syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT} c := make(chan os.Signal, 1) signal.Notify(c, terminationSignals...) // Block until a signal is received. log.Println("Now listening for interrupts") s := <-c log.Printf("Got signal: %v. Sending down to process (PID: %v)", s, process.Pid) if err := process.Signal(s); err != nil { log.Fatalf("Failed to signal process: %v", err) } log.Printf("Signalled process %v successfully.", process.Pid) } kubernetes-component-base-1b2882b/logs/logreduction/000077500000000000000000000000001476422213000225665ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/logreduction/logreduction.go000066400000000000000000000046431476422213000256220ustar00rootroot00000000000000/* Copyright 2018 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 logreduction import ( "sync" "time" ) var nowfunc = func() time.Time { return time.Now() } // LogReduction provides a filter for consecutive identical log messages; // a message will be printed no more than once per interval. // If a string of messages is interrupted by a different message, // the interval timer will be reset. type LogReduction struct { lastError map[string]string errorPrinted map[string]time.Time errorMapLock sync.Mutex identicalErrorDelay time.Duration } // NewLogReduction returns an initialized LogReduction func NewLogReduction(identicalErrorDelay time.Duration) *LogReduction { l := new(LogReduction) l.lastError = make(map[string]string) l.errorPrinted = make(map[string]time.Time) l.identicalErrorDelay = identicalErrorDelay return l } func (l *LogReduction) cleanupErrorTimeouts() { for name, timeout := range l.errorPrinted { if nowfunc().Sub(timeout) >= l.identicalErrorDelay { delete(l.errorPrinted, name) delete(l.lastError, name) } } } // ShouldMessageBePrinted determines whether a message should be printed based // on how long ago this particular message was last printed func (l *LogReduction) ShouldMessageBePrinted(message string, parentID string) bool { l.errorMapLock.Lock() defer l.errorMapLock.Unlock() l.cleanupErrorTimeouts() lastMsg, ok := l.lastError[parentID] lastPrinted, ok1 := l.errorPrinted[parentID] if !ok || !ok1 || message != lastMsg || nowfunc().Sub(lastPrinted) >= l.identicalErrorDelay { l.errorPrinted[parentID] = nowfunc() l.lastError[parentID] = message return true } return false } // ClearID clears out log reduction records pertaining to a particular parent // (e. g. container ID) func (l *LogReduction) ClearID(parentID string) { l.errorMapLock.Lock() defer l.errorMapLock.Unlock() delete(l.lastError, parentID) delete(l.errorPrinted, parentID) } kubernetes-component-base-1b2882b/logs/logreduction/logreduction_test.go000066400000000000000000000036441476422213000266610ustar00rootroot00000000000000/* Copyright 2018 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 logreduction import ( "testing" "time" ) var time0 = time.Unix(1000, 0) var time1 = time.Unix(1001, 0) var time2 = time.Unix(1012, 0) var identicalErrorDelay = 10 * time.Second var testCount = 0 const ( mesg1 = "This is a message" mesg2 = "This is not a message" id1 = "Container1" id2 = "Container2" ) func checkThat(t *testing.T, r *LogReduction, m, id string) { testCount++ if !r.ShouldMessageBePrinted(m, id) { t.Errorf("Case %d failed (%s/%s should be printed)", testCount, m, id) } } func checkThatNot(t *testing.T, r *LogReduction, m, id string) { testCount++ if r.ShouldMessageBePrinted(m, id) { t.Errorf("Case %d failed (%s/%s should not be printed)", testCount, m, id) } } func TestLogReduction(t *testing.T) { var timeToReturn = time0 nowfunc = func() time.Time { return timeToReturn } r := NewLogReduction(identicalErrorDelay) checkThat(t, r, mesg1, id1) // 1 checkThatNot(t, r, mesg1, id1) // 2 checkThat(t, r, mesg1, id2) // 3 checkThatNot(t, r, mesg1, id1) // 4 timeToReturn = time1 checkThatNot(t, r, mesg1, id1) // 5 timeToReturn = time2 checkThat(t, r, mesg1, id1) // 6 checkThatNot(t, r, mesg1, id1) // 7 checkThat(t, r, mesg2, id1) // 8 checkThat(t, r, mesg1, id1) // 9 checkThat(t, r, mesg1, id2) // 10 r.ClearID(id1) checkThat(t, r, mesg1, id1) // 11 checkThatNot(t, r, mesg1, id2) // 12 } kubernetes-component-base-1b2882b/logs/logs.go000066400000000000000000000146111476422213000213660ustar00rootroot00000000000000/* Copyright 2014 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 logs contains support for logging options, flags and setup. // Commands must explicitly enable command line flags. They no longer // get added automatically when importing this package. package logs import ( "flag" "fmt" "log" "strconv" "time" "github.com/spf13/pflag" logsapi "k8s.io/component-base/logs/api/v1" "k8s.io/component-base/logs/internal/setverbositylevel" "k8s.io/component-base/logs/klogflags" "k8s.io/klog/v2" ) const vmoduleUsage = " (only works for the default text log format)" var ( packageFlags = flag.NewFlagSet("logging", flag.ContinueOnError) // Periodic flushing gets configured either via the global flag // in this file or via LoggingConfiguration. logFlushFreq time.Duration ) func init() { klogflags.Init(packageFlags) packageFlags.DurationVar(&logFlushFreq, logsapi.LogFlushFreqFlagName, logsapi.LogFlushFreqDefault, "Maximum number of seconds between log flushes") } type addFlagsOptions struct { skipLoggingConfigurationFlags bool } type Option func(*addFlagsOptions) // SkipLoggingConfigurationFlags must be used as option for AddFlags when // the program also uses a LoggingConfiguration struct for configuring // logging. Then only flags not covered by that get added. func SkipLoggingConfigurationFlags() Option { return func(o *addFlagsOptions) { o.skipLoggingConfigurationFlags = true } } // Options is an alias for LoggingConfiguration to comply with component-base // conventions. type Options = logsapi.LoggingConfiguration // NewOptions is an alias for NewLoggingConfiguration. var NewOptions = logsapi.NewLoggingConfiguration // AddFlags registers this package's flags on arbitrary FlagSets. This includes // the klog flags, with the original underscore as separator between. If // commands want hyphens as separators, they can set // k8s.io/component-base/cli/flag/WordSepNormalizeFunc as normalization // function on the flag set before calling AddFlags. // // May be called more than once. func AddFlags(fs *pflag.FlagSet, opts ...Option) { o := addFlagsOptions{} for _, opt := range opts { opt(&o) } // Add all supported flags. packageFlags.VisitAll(func(f *flag.Flag) { pf := pflag.PFlagFromGoFlag(f) switch f.Name { case "v", logsapi.LogFlushFreqFlagName: // unchanged, potentially skip it if o.skipLoggingConfigurationFlags { return } case "vmodule": if o.skipLoggingConfigurationFlags { return } pf.Usage += vmoduleUsage } if fs.Lookup(pf.Name) == nil { fs.AddFlag(pf) } }) } // AddGoFlags is a variant of AddFlags for traditional Go flag.FlagSet. // Commands should use pflag whenever possible for the sake of consistency. // Cases where this function is needed include tests (they have to set up flags // in flag.CommandLine) and commands that for historic reasons use Go // flag.Parse and cannot change to pflag because it would break their command // line interface. func AddGoFlags(fs *flag.FlagSet, opts ...Option) { o := addFlagsOptions{} for _, opt := range opts { opt(&o) } // Add flags with deprecation remark added to the usage text of // some klog flags. packageFlags.VisitAll(func(f *flag.Flag) { usage := f.Usage switch f.Name { case "v", logsapi.LogFlushFreqFlagName: // unchanged if o.skipLoggingConfigurationFlags { return } case "vmodule": if o.skipLoggingConfigurationFlags { return } usage += vmoduleUsage } fs.Var(f.Value, f.Name, usage) }) } // KlogWriter serves as a bridge between the standard log package and the glog package. type KlogWriter struct{} // Write implements the io.Writer interface. func (writer KlogWriter) Write(data []byte) (n int, err error) { klog.InfoDepth(1, string(data)) return len(data), nil } // InitLogs initializes logs the way we want for Kubernetes. // It should be called after parsing flags. If called before that, // it will use the default log settings. // // InitLogs disables support for contextual logging in klog while // that Kubernetes feature is not considered stable yet. Commands // which want to support contextual logging can: // - call klog.EnableContextualLogging after calling InitLogs, // with a fixed `true` or depending on some command line flag or // a feature gate check // - set up a FeatureGate instance, the advanced logging configuration // with Options and call Options.ValidateAndApply with the FeatureGate; // k8s.io/component-base/logs/example/cmd demonstrates how to do that func InitLogs() { log.SetOutput(KlogWriter{}) log.SetFlags(0) // Start flushing now. If LoggingConfiguration.ApplyAndValidate is // used, it will restart the daemon with the log flush interval defined // there. klog.StartFlushDaemon(logFlushFreq) // This is the default in Kubernetes. Options.ValidateAndApply // will override this with the result of a feature gate check. klog.EnableContextualLogging(false) } // FlushLogs flushes logs immediately. This should be called at the end of // the main function via defer to ensure that all pending log messages // are printed before exiting the program. func FlushLogs() { klog.Flush() } // NewLogger creates a new log.Logger which sends logs to klog.Info. func NewLogger(prefix string) *log.Logger { return log.New(KlogWriter{}, prefix, 0) } // GlogSetter modifies the verbosity threshold for the entire program. // Some components have HTTP-based APIs for invoking this at runtime. func GlogSetter(val string) (string, error) { v, err := strconv.ParseUint(val, 10, 32) if err != nil { return "", err } var level klog.Level if err := level.Set(val); err != nil { return "", fmt.Errorf("failed set klog.logging.verbosity %s: %v", val, err) } setverbositylevel.Mutex.Lock() defer setverbositylevel.Mutex.Unlock() for _, cb := range setverbositylevel.Callbacks { if err := cb(uint32(v)); err != nil { return "", err } } return fmt.Sprintf("successfully set klog.logging.verbosity to %s", val), nil } kubernetes-component-base-1b2882b/logs/logs_test.go000066400000000000000000000060551476422213000224300ustar00rootroot00000000000000/* Copyright 2022 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 logs_test import ( "os" "path" "testing" "k8s.io/component-base/logs" logsapi "k8s.io/component-base/logs/api/v1" _ "k8s.io/component-base/logs/json/register" "k8s.io/klog/v2" ) func TestGlogSetter(t *testing.T) { testcases := map[string]struct { init func(t *testing.T) // write must write at verbosity level 1. write func() }{ "klog": { init: func(t *testing.T) {}, write: func() { klog.V(1).Info("hello") }, }, "json": { init: func(t *testing.T) { c := logsapi.NewLoggingConfiguration() c.Format = "json" if err := logsapi.ValidateAndApply(c, nil /* feature gates */); err != nil { t.Fatalf("Unexpected error enabling json output: %v", err) } }, write: func() { klog.Background().V(1).Info("hello") }, }, "text": { init: func(t *testing.T) { c := logsapi.NewLoggingConfiguration() c.Format = "text" if err := logsapi.ValidateAndApply(c, nil /* feature gates */); err != nil { t.Fatalf("Unexpected error enabling text output: %v", err) } }, write: func() { klog.Background().V(1).Info("hello") }, }, } for name, tc := range testcases { t.Run(name, func(t *testing.T) { state := klog.CaptureState() t.Cleanup(state.Restore) tmpdir := t.TempDir() tmpfile := path.Join(tmpdir, "stderr.log") oldStderr := os.Stderr defer func() { os.Stderr = oldStderr }() newStderr, err := os.Create(tmpfile) if err != nil { t.Fatalf("Unexpected error creating temp file: %v", err) } os.Stderr = newStderr defer func() { if err := logsapi.ResetForTest(nil /* feature gates */); err != nil { t.Errorf("Unexpected error resetting the logging configuration: %v", err) } }() tc.init(t) // First write with default verbosity level of 0 -> no output. tc.write() klog.Flush() out, err := os.ReadFile(tmpfile) if err != nil { t.Fatalf("Unexpected error reading temp file: %v", err) } if len(out) > 0 { t.Fatalf("Info message should have been discarded, got instead:\n%s", string(out)) } // Increase verbosity at runtime. if _, err := logs.GlogSetter("1"); err != nil { t.Fatalf("Unexpected error setting verbosity level: %v", err) } // Now write again -> output. tc.write() klog.Flush() out, err = os.ReadFile(tmpfile) if err != nil { t.Fatalf("Unexpected error reading temp file: %v", err) } if len(out) == 0 { t.Fatal("Info message should have been written, got empty file instead.") } }) } } kubernetes-component-base-1b2882b/logs/testinit/000077500000000000000000000000001476422213000217335ustar00rootroot00000000000000kubernetes-component-base-1b2882b/logs/testinit/testinit.go000066400000000000000000000016721476422213000241330ustar00rootroot00000000000000/* Copyright 2021 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 testinit adds logging flags to a Ginkgo or Go test program during // initialization, something that the logs package itself no longer does. // // Normal commands should not use this and instead manage logging flags with // logs.Options and/or cli.Run. package testinit import ( "flag" "k8s.io/component-base/logs" ) func init() { logs.AddGoFlags(flag.CommandLine) } kubernetes-component-base-1b2882b/metrics/000077500000000000000000000000001476422213000205725ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/OWNERS000066400000000000000000000003321476422213000215300ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners approvers: - sig-instrumentation-approvers - logicalhan - RainbowMango reviewers: - sig-instrumentation-reviewers - YoyinZyc labels: - sig/instrumentation kubernetes-component-base-1b2882b/metrics/buckets.go000066400000000000000000000034441476422213000225660ustar00rootroot00000000000000/* Copyright 2022 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 metrics import ( "github.com/prometheus/client_golang/prometheus" ) // DefBuckets is a wrapper for prometheus.DefBuckets var DefBuckets = prometheus.DefBuckets // LinearBuckets is a wrapper for prometheus.LinearBuckets. func LinearBuckets(start, width float64, count int) []float64 { return prometheus.LinearBuckets(start, width, count) } // ExponentialBuckets is a wrapper for prometheus.ExponentialBuckets. func ExponentialBuckets(start, factor float64, count int) []float64 { return prometheus.ExponentialBuckets(start, factor, count) } // ExponentialBucketsRange creates 'count' buckets, where the lowest bucket is // 'min' and the highest bucket is 'max'. The final +Inf bucket is not counted // and not included in the returned slice. The returned slice is meant to be // used for the Buckets field of HistogramOpts. // // The function panics if 'count' is 0 or negative, if 'min' is 0 or negative. func ExponentialBucketsRange(min, max float64, count int) []float64 { return prometheus.ExponentialBucketsRange(min, max, count) } // MergeBuckets merges buckets together func MergeBuckets(buckets ...[]float64) []float64 { result := make([]float64, 1) for _, s := range buckets { result = append(result, s...) } return result } kubernetes-component-base-1b2882b/metrics/collector.go000066400000000000000000000127631476422213000231200ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "fmt" "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" ) // StableCollector extends the prometheus.Collector interface to allow customization of the // metric registration process, it's especially intend to be used in scenario of custom collector. type StableCollector interface { prometheus.Collector // DescribeWithStability sends the super-set of all possible metrics.Desc collected // by this StableCollector to the provided channel. DescribeWithStability(chan<- *Desc) // CollectWithStability sends each collected metrics.Metric via the provide channel. CollectWithStability(chan<- Metric) // Create will initialize all Desc and it intends to be called by registry. Create(version *semver.Version, self StableCollector) bool // ClearState will clear all the states marked by Create. ClearState() // HiddenMetrics tells the list of hidden metrics with fqName. HiddenMetrics() []string } // BaseStableCollector which implements almost all methods defined by StableCollector // is a convenient assistant for custom collectors. // It is recommended to inherit BaseStableCollector when implementing custom collectors. type BaseStableCollector struct { descriptors map[string]*Desc // stores all descriptors by pair, these are collected from DescribeWithStability(). registerable map[string]*Desc // stores registerable descriptors by pair, is a subset of descriptors. hidden map[string]*Desc // stores hidden descriptors by pair, is a subset of descriptors. self StableCollector } // DescribeWithStability sends all descriptors to the provided channel. // Every custom collector should over-write this method. func (bsc *BaseStableCollector) DescribeWithStability(ch chan<- *Desc) { panic(fmt.Errorf("custom collector should over-write DescribeWithStability method")) } // Describe sends all descriptors to the provided channel. // It intended to be called by prometheus registry. func (bsc *BaseStableCollector) Describe(ch chan<- *prometheus.Desc) { for _, d := range bsc.registerable { ch <- d.toPrometheusDesc() } } // CollectWithStability sends all metrics to the provided channel. // Every custom collector should over-write this method. func (bsc *BaseStableCollector) CollectWithStability(ch chan<- Metric) { panic(fmt.Errorf("custom collector should over-write CollectWithStability method")) } // Collect is called by the Prometheus registry when collecting metrics. func (bsc *BaseStableCollector) Collect(ch chan<- prometheus.Metric) { mch := make(chan Metric) go func() { bsc.self.CollectWithStability(mch) close(mch) }() for m := range mch { // nil Metric usually means hidden metrics if m == nil { continue } ch <- prometheus.Metric(m) } } func (bsc *BaseStableCollector) add(d *Desc) { if len(d.fqName) == 0 { panic("nameless metrics will be not allowed") } if bsc.descriptors == nil { bsc.descriptors = make(map[string]*Desc) } if _, exist := bsc.descriptors[d.fqName]; exist { panic(fmt.Sprintf("duplicate metrics (%s) will be not allowed", d.fqName)) } bsc.descriptors[d.fqName] = d } // Init intends to be called by registry. func (bsc *BaseStableCollector) init(self StableCollector) { bsc.self = self dch := make(chan *Desc) // collect all possible descriptions from custom side go func() { bsc.self.DescribeWithStability(dch) close(dch) }() for d := range dch { bsc.add(d) } } func (bsc *BaseStableCollector) trackRegistrableDescriptor(d *Desc) { if bsc.registerable == nil { bsc.registerable = make(map[string]*Desc) } bsc.registerable[d.fqName] = d } func (bsc *BaseStableCollector) trackHiddenDescriptor(d *Desc) { if bsc.hidden == nil { bsc.hidden = make(map[string]*Desc) } bsc.hidden[d.fqName] = d } // Create intends to be called by registry. // Create will return true as long as there is one or more metrics not be hidden. // Otherwise return false, that means the whole collector will be ignored by registry. func (bsc *BaseStableCollector) Create(version *semver.Version, self StableCollector) bool { bsc.init(self) for _, d := range bsc.descriptors { d.create(version) if d.IsHidden() { bsc.trackHiddenDescriptor(d) } else { bsc.trackRegistrableDescriptor(d) } } if len(bsc.registerable) > 0 { return true } return false } // ClearState will clear all the states marked by Create. // It intends to be used for re-register a hidden metric. func (bsc *BaseStableCollector) ClearState() { for _, d := range bsc.descriptors { d.ClearState() } bsc.descriptors = nil bsc.registerable = nil bsc.hidden = nil bsc.self = nil } // HiddenMetrics tells the list of hidden metrics with fqName. func (bsc *BaseStableCollector) HiddenMetrics() (fqNames []string) { for i := range bsc.hidden { fqNames = append(fqNames, bsc.hidden[i].fqName) } return } // Check if our BaseStableCollector implements necessary interface var _ StableCollector = &BaseStableCollector{} kubernetes-component-base-1b2882b/metrics/collector_test.go000066400000000000000000000127621476422213000241560ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "fmt" "reflect" "strings" "testing" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" apimachineryversion "k8s.io/apimachinery/pkg/version" ) type testCustomCollector struct { BaseStableCollector descriptors []*Desc } func newTestCustomCollector(ds ...*Desc) *testCustomCollector { c := &testCustomCollector{} c.descriptors = append(c.descriptors, ds...) return c } func (tc *testCustomCollector) DescribeWithStability(ch chan<- *Desc) { for i := range tc.descriptors { ch <- tc.descriptors[i] } } func (tc *testCustomCollector) CollectWithStability(ch chan<- Metric) { for i := range tc.descriptors { ch <- NewLazyConstMetric(tc.descriptors[i], GaugeValue, 1, "value") } } func TestBaseCustomCollector(t *testing.T) { var currentVersion = apimachineryversion.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.0-alpha-1.12345", } var ( alphaDesc = NewDesc("metric_alpha", "alpha metric", []string{"name"}, nil, ALPHA, "") internalDesc = NewDesc("metric_internal", "internal metrics", []string{"name"}, nil, INTERNAL, "") stableDesc = NewDesc("metric_stable", "stable metrics", []string{"name"}, nil, STABLE, "") deprecatedDesc = NewDesc("metric_deprecated", "stable deprecated metrics", []string{"name"}, nil, STABLE, "1.17.0") hiddenDesc = NewDesc("metric_hidden", "stable hidden metrics", []string{"name"}, nil, STABLE, "1.16.0") ) registry := newKubeRegistry(currentVersion) customCollector := newTestCustomCollector(alphaDesc, internalDesc, stableDesc, deprecatedDesc, hiddenDesc) if err := registry.CustomRegister(customCollector); err != nil { t.Fatalf("register collector failed with err: %v", err) } expectedMetrics := ` # HELP metric_alpha [ALPHA] alpha metric # TYPE metric_alpha gauge metric_alpha{name="value"} 1 # HELP metric_internal [INTERNAL] internal metrics # TYPE metric_internal gauge metric_internal{name="value"} 1 # HELP metric_stable [STABLE] stable metrics # TYPE metric_stable gauge metric_stable{name="value"} 1 # HELP metric_deprecated [STABLE] (Deprecated since 1.17.0) stable deprecated metrics # TYPE metric_deprecated gauge metric_deprecated{name="value"} 1 ` err := testutil.GatherAndCompare(registry, strings.NewReader(expectedMetrics), alphaDesc.fqName, internalDesc.fqName, stableDesc.fqName, deprecatedDesc.fqName, hiddenDesc.fqName) if err != nil { t.Fatal(err) } } func TestInvalidCustomCollector(t *testing.T) { var currentVersion = apimachineryversion.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.0-alpha-1.12345", } var namelessDesc = NewDesc("", "this is a nameless metric", nil, nil, ALPHA, "") var duplicatedDescA = NewDesc("test_duplicated_metric", "this is a duplicated metric A", nil, nil, ALPHA, "") var duplicatedDescB = NewDesc("test_duplicated_metric", "this is a duplicated metric B", nil, nil, ALPHA, "") var tests = []struct { name string descriptors []*Desc panicStr string }{ { name: "nameless metric will be not allowed", descriptors: []*Desc{namelessDesc}, panicStr: "nameless metrics will be not allowed", }, { name: "duplicated metric will be not allowed", descriptors: []*Desc{duplicatedDescA, duplicatedDescB}, panicStr: fmt.Sprintf("duplicate metrics (%s) will be not allowed", duplicatedDescA.fqName), }, } for _, test := range tests { tc := test t.Run(tc.name, func(t *testing.T) { registry := newKubeRegistry(currentVersion) customCollector := newTestCustomCollector(tc.descriptors...) assert.Panics(t, func() { registry.CustomMustRegister(customCollector) }, tc.panicStr) }) } } // TestCustomCollectorClearState guarantees `ClearState()` will fully clear a collector. // It is necessary because we may forget to clear some new-added fields in the future. func TestCustomCollectorClearState(t *testing.T) { var currentVersion = parseVersion(apimachineryversion.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.0-alpha-1.12345", }) var ( alphaDesc = NewDesc("metric_alpha", "alpha metric", []string{"name"}, nil, ALPHA, "") stableDesc = NewDesc("metric_stable", "stable metrics", []string{"name"}, nil, STABLE, "") deprecatedDesc = NewDesc("metric_deprecated", "stable deprecated metrics", []string{"name"}, nil, STABLE, "1.17.0") hiddenDesc = NewDesc("metric_hidden", "stable hidden metrics", []string{"name"}, nil, STABLE, "1.16.0") ) benchmarkA := newTestCustomCollector(alphaDesc, stableDesc, deprecatedDesc, hiddenDesc) benchmarkB := newTestCustomCollector(alphaDesc, stableDesc, deprecatedDesc, hiddenDesc) if benchmarkA.Create(¤tVersion, benchmarkA) == false { t.Fatal("collector should be created") } benchmarkA.ClearState() if !reflect.DeepEqual(*benchmarkA, *benchmarkB) { t.Fatal("custom collector state hasn't be fully cleared") } } kubernetes-component-base-1b2882b/metrics/counter.go000066400000000000000000000240471476422213000226070ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "context" "sync" "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/trace" dto "github.com/prometheus/client_model/go" ) // Counter is our internal representation for our wrapping struct around prometheus // counters. Counter implements both kubeCollector and CounterMetric. type Counter struct { ctx context.Context CounterMetric *CounterOpts lazyMetric selfCollector } // The implementation of the Metric interface is expected by testutil.GetCounterMetricValue. var _ Metric = &Counter{} // All supported exemplar metric types implement the metricWithExemplar interface. var _ metricWithExemplar = &Counter{} // exemplarCounterMetric holds a context to extract exemplar labels from, and a counter metric to attach them to. It implements the metricWithExemplar interface. type exemplarCounterMetric struct { *Counter } // NewCounter returns an object which satisfies the kubeCollector and CounterMetric interfaces. // However, the object returned will not measure anything unless the collector is first // registered, since the metric is lazily instantiated. func NewCounter(opts *CounterOpts) *Counter { opts.StabilityLevel.setDefaults() kc := &Counter{ CounterOpts: opts, lazyMetric: lazyMetric{stabilityLevel: opts.StabilityLevel}, } kc.setPrometheusCounter(noop) kc.lazyInit(kc, BuildFQName(opts.Namespace, opts.Subsystem, opts.Name)) return kc } func (c *Counter) Desc() *prometheus.Desc { return c.metric.Desc() } func (c *Counter) Write(to *dto.Metric) error { return c.metric.Write(to) } // Reset resets the underlying prometheus Counter to start counting from 0 again func (c *Counter) Reset() { if !c.IsCreated() { return } c.setPrometheusCounter(prometheus.NewCounter(c.CounterOpts.toPromCounterOpts())) } // setPrometheusCounter sets the underlying CounterMetric object, i.e. the thing that does the measurement. func (c *Counter) setPrometheusCounter(counter prometheus.Counter) { c.CounterMetric = counter c.initSelfCollection(counter) } // DeprecatedVersion returns a pointer to the Version or nil func (c *Counter) DeprecatedVersion() *semver.Version { return parseSemver(c.CounterOpts.DeprecatedVersion) } // initializeMetric invocation creates the actual underlying Counter. Until this method is called // the underlying counter is a no-op. func (c *Counter) initializeMetric() { c.CounterOpts.annotateStabilityLevel() // this actually creates the underlying prometheus counter. c.setPrometheusCounter(prometheus.NewCounter(c.CounterOpts.toPromCounterOpts())) } // initializeDeprecatedMetric invocation creates the actual (but deprecated) Counter. Until this method // is called the underlying counter is a no-op. func (c *Counter) initializeDeprecatedMetric() { c.CounterOpts.markDeprecated() c.initializeMetric() } // WithContext allows the normal Counter metric to pass in context. func (c *Counter) WithContext(ctx context.Context) CounterMetric { c.ctx = ctx return c.CounterMetric } // withExemplar initializes the exemplarMetric object and sets the exemplar value. func (c *Counter) withExemplar(v float64) { (&exemplarCounterMetric{c}).withExemplar(v) } func (c *Counter) Add(v float64) { c.withExemplar(v) } func (c *Counter) Inc() { c.withExemplar(1) } // withExemplar attaches an exemplar to the metric. func (e *exemplarCounterMetric) withExemplar(v float64) { if m, ok := e.CounterMetric.(prometheus.ExemplarAdder); ok { maybeSpanCtx := trace.SpanContextFromContext(e.ctx) if maybeSpanCtx.IsValid() && maybeSpanCtx.IsSampled() { exemplarLabels := prometheus.Labels{ "trace_id": maybeSpanCtx.TraceID().String(), "span_id": maybeSpanCtx.SpanID().String(), } m.AddWithExemplar(v, exemplarLabels) return } } e.CounterMetric.Add(v) } // CounterVec is the internal representation of our wrapping struct around prometheus // counterVecs. CounterVec implements both kubeCollector and CounterVecMetric. type CounterVec struct { *prometheus.CounterVec *CounterOpts lazyMetric originalLabels []string } var _ kubeCollector = &CounterVec{} // TODO: make this true: var _ CounterVecMetric = &CounterVec{} // NewCounterVec returns an object which satisfies the kubeCollector and (almost) CounterVecMetric interfaces. // However, the object returned will not measure anything unless the collector is first // registered, since the metric is lazily instantiated, and only members extracted after // registration will actually measure anything. func NewCounterVec(opts *CounterOpts, labels []string) *CounterVec { opts.StabilityLevel.setDefaults() fqName := BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) cv := &CounterVec{ CounterVec: noopCounterVec, CounterOpts: opts, originalLabels: labels, lazyMetric: lazyMetric{stabilityLevel: opts.StabilityLevel}, } cv.lazyInit(cv, fqName) return cv } // DeprecatedVersion returns a pointer to the Version or nil func (v *CounterVec) DeprecatedVersion() *semver.Version { return parseSemver(v.CounterOpts.DeprecatedVersion) } // initializeMetric invocation creates the actual underlying CounterVec. Until this method is called // the underlying counterVec is a no-op. func (v *CounterVec) initializeMetric() { v.CounterOpts.annotateStabilityLevel() v.CounterVec = prometheus.NewCounterVec(v.CounterOpts.toPromCounterOpts(), v.originalLabels) } // initializeDeprecatedMetric invocation creates the actual (but deprecated) CounterVec. Until this method is called // the underlying counterVec is a no-op. func (v *CounterVec) initializeDeprecatedMetric() { v.CounterOpts.markDeprecated() v.initializeMetric() } // Default Prometheus Vec behavior is that member extraction results in creation of a new element // if one with the unique label values is not found in the underlying stored metricMap. // This means that if this function is called but the underlying metric is not registered // (which means it will never be exposed externally nor consumed), the metric will exist in memory // for perpetuity (i.e. throughout application lifecycle). // // For reference: https://github.com/prometheus/client_golang/blob/v0.9.2/prometheus/counter.go#L179-L197 // // In contrast, the Vec behavior in this package is that member extraction before registration // returns a permanent noop object. // WithLabelValues returns the Counter for the given slice of label // values (same order as the VariableLabels in Desc). If that combination of // label values is accessed for the first time, a new Counter is created IFF the counterVec // has been registered to a metrics registry. func (v *CounterVec) WithLabelValues(lvs ...string) CounterMetric { if !v.IsCreated() { return noop // return no-op counter } if v.LabelValueAllowLists != nil { v.LabelValueAllowLists.ConstrainToAllowedList(v.originalLabels, lvs) } else { v.initializeLabelAllowListsOnce.Do(func() { allowListLock.RLock() if allowList, ok := labelValueAllowLists[v.FQName()]; ok { v.LabelValueAllowLists = allowList allowList.ConstrainToAllowedList(v.originalLabels, lvs) } allowListLock.RUnlock() }) } return v.CounterVec.WithLabelValues(lvs...) } // With returns the Counter for the given Labels map (the label names // must match those of the VariableLabels in Desc). If that label map is // accessed for the first time, a new Counter is created IFF the counterVec has // been registered to a metrics registry. func (v *CounterVec) With(labels map[string]string) CounterMetric { if !v.IsCreated() { return noop // return no-op counter } if v.LabelValueAllowLists != nil { v.LabelValueAllowLists.ConstrainLabelMap(labels) } else { v.initializeLabelAllowListsOnce.Do(func() { allowListLock.RLock() if allowList, ok := labelValueAllowLists[v.FQName()]; ok { v.LabelValueAllowLists = allowList allowList.ConstrainLabelMap(labels) } allowListLock.RUnlock() }) } return v.CounterVec.With(labels) } // Delete deletes the metric where the variable labels are the same as those // passed in as labels. It returns true if a metric was deleted. // // It is not an error if the number and names of the Labels are inconsistent // with those of the VariableLabels in Desc. However, such inconsistent Labels // can never match an actual metric, so the method will always return false in // that case. func (v *CounterVec) Delete(labels map[string]string) bool { if !v.IsCreated() { return false // since we haven't created the metric, we haven't deleted a metric with the passed in values } return v.CounterVec.Delete(labels) } // Reset deletes all metrics in this vector. func (v *CounterVec) Reset() { if !v.IsCreated() { return } v.CounterVec.Reset() } // ResetLabelAllowLists resets the label allow list for the CounterVec. // NOTE: This should only be used in test. func (v *CounterVec) ResetLabelAllowLists() { v.initializeLabelAllowListsOnce = sync.Once{} v.LabelValueAllowLists = nil } // WithContext returns wrapped CounterVec with context func (v *CounterVec) WithContext(ctx context.Context) *CounterVecWithContext { return &CounterVecWithContext{ ctx: ctx, CounterVec: v, } } // CounterVecWithContext is the wrapper of CounterVec with context. type CounterVecWithContext struct { *CounterVec ctx context.Context } // WithLabelValues is the wrapper of CounterVec.WithLabelValues. func (vc *CounterVecWithContext) WithLabelValues(lvs ...string) CounterMetric { return vc.CounterVec.WithLabelValues(lvs...) } // With is the wrapper of CounterVec.With. func (vc *CounterVecWithContext) With(labels map[string]string) CounterMetric { return vc.CounterVec.With(labels) } kubernetes-component-base-1b2882b/metrics/counter_test.go000066400000000000000000000260721476422213000236460ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "bytes" "context" "testing" "github.com/blang/semver/v4" dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" apimachineryversion "k8s.io/apimachinery/pkg/version" ) func TestCounter(t *testing.T) { var tests = []struct { desc string *CounterOpts expectedMetricCount int expectedHelp string }{ { desc: "Test non deprecated", CounterOpts: &CounterOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", StabilityLevel: ALPHA, Help: "counter help", }, expectedMetricCount: 1, expectedHelp: "[ALPHA] counter help", }, { desc: "Test deprecated", CounterOpts: &CounterOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "counter help", StabilityLevel: ALPHA, DeprecatedVersion: "1.15.0", }, expectedMetricCount: 1, expectedHelp: "[ALPHA] (Deprecated since 1.15.0) counter help", }, { desc: "Test hidden", CounterOpts: &CounterOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "counter help", StabilityLevel: ALPHA, DeprecatedVersion: "1.14.0", }, expectedMetricCount: 0, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) // c is a pointer to a Counter c := NewCounter(test.CounterOpts) registry.MustRegister(c) // mfs is a pointer to a dto.MetricFamily slice mfs, err := registry.Gather() var buf bytes.Buffer enc := expfmt.NewEncoder(&buf, "text/plain; version=0.0.4; charset=utf-8") assert.Lenf(t, mfs, test.expectedMetricCount, "Got %v metrics, Want: %v metrics", len(mfs), test.expectedMetricCount) require.NoError(t, err, "Gather failed %v", err) for _, metric := range mfs { err := enc.Encode(metric) require.NoError(t, err, "Unexpected err %v in encoding the metric", err) assert.Equalf(t, test.expectedHelp, metric.GetHelp(), "Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) } // increment the counter N number of times and verify that the metric retains the count correctly numberOfTimesToIncrement := 3 for i := 0; i < numberOfTimesToIncrement; i++ { c.Inc() } mfs, err = registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range mfs { mfMetric := mf.GetMetric() for _, m := range mfMetric { assert.Equalf(t, numberOfTimesToIncrement, int(m.GetCounter().GetValue()), "Got %v, wanted %v as the count", m.GetCounter().GetValue(), numberOfTimesToIncrement) } } }) } } func TestCounterVec(t *testing.T) { var tests = []struct { desc string *CounterOpts labels []string registryVersion *semver.Version expectedMetricFamilyCount int expectedHelp string }{ { desc: "Test non deprecated", CounterOpts: &CounterOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "counter help", }, labels: []string{"label_a", "label_b"}, expectedMetricFamilyCount: 1, expectedHelp: "[ALPHA] counter help", }, { desc: "Test deprecated", CounterOpts: &CounterOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "counter help", DeprecatedVersion: "1.15.0", }, labels: []string{"label_a", "label_b"}, expectedMetricFamilyCount: 1, expectedHelp: "[ALPHA] (Deprecated since 1.15.0) counter help", }, { desc: "Test hidden", CounterOpts: &CounterOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "counter help", DeprecatedVersion: "1.14.0", }, labels: []string{"label_a", "label_b"}, expectedMetricFamilyCount: 0, expectedHelp: "counter help", }, { desc: "Test alpha", CounterOpts: &CounterOpts{ StabilityLevel: ALPHA, Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "counter help", }, labels: []string{"label_a", "label_b"}, expectedMetricFamilyCount: 1, expectedHelp: "[ALPHA] counter help", }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) c := NewCounterVec(test.CounterOpts, test.labels) registry.MustRegister(c) c.WithLabelValues("1", "2").Inc() mfs, err := registry.Gather() assert.Lenf(t, mfs, test.expectedMetricFamilyCount, "Got %v metric families, Want: %v metric families", len(mfs), test.expectedMetricFamilyCount) require.NoError(t, err, "Gather failed %v", err) // this no-opts here when there are no metric families (i.e. when the metric is hidden) for _, mf := range mfs { assert.Lenf(t, mf.GetMetric(), 1, "Got %v metrics, wanted 1 as the count", len(mf.GetMetric())) assert.Equalf(t, test.expectedHelp, mf.GetHelp(), "Got %s as help message, want %s", mf.GetHelp(), test.expectedHelp) } // let's increment the counter and verify that the metric still works c.WithLabelValues("1", "3").Inc() c.WithLabelValues("2", "3").Inc() mfs, err = registry.Gather() require.NoError(t, err, "Gather failed %v", err) // this no-opts here when there are no metric families (i.e. when the metric is hidden) for _, mf := range mfs { assert.Lenf(t, mf.GetMetric(), 3, "Got %v metrics, wanted 3 as the count", len(mf.GetMetric())) } }) } } func TestCounterWithLabelValueAllowList(t *testing.T) { labelAllowValues := map[string]string{ "namespace_subsystem_metric_allowlist_test,label_a": "allowed", } labels := []string{"label_a", "label_b"} opts := &CounterOpts{ Namespace: "namespace", Name: "metric_allowlist_test", Subsystem: "subsystem", } var tests = []struct { desc string labelValues [][]string expectMetricValues map[string]int }{ { desc: "Test no unexpected input", labelValues: [][]string{{"allowed", "b1"}, {"allowed", "b2"}}, expectMetricValues: map[string]int{ "allowed b1": 1, "allowed b2": 1, }, }, { desc: "Test unexpected input", labelValues: [][]string{{"allowed", "b1"}, {"not_allowed", "b1"}}, expectMetricValues: map[string]int{ "allowed b1": 1, "unexpected b1": 1, }, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { labelValueAllowLists = map[string]*MetricLabelAllowList{} registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) c := NewCounterVec(opts, labels) registry.MustRegister(c) SetLabelAllowListFromCLI(labelAllowValues) for _, lv := range test.labelValues { c.WithLabelValues(lv...).Inc() } mfs, err := registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range mfs { if *mf.Name != BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) { continue } mfMetric := mf.GetMetric() for _, m := range mfMetric { var aValue, bValue string for _, l := range m.Label { if *l.Name == "label_a" { aValue = *l.Value } if *l.Name == "label_b" { bValue = *l.Value } } labelValuePair := aValue + " " + bValue expectedValue, ok := test.expectMetricValues[labelValuePair] assert.True(t, ok, "Got unexpected label values, lable_a is %v, label_b is %v", aValue, bValue) actualValue := int(m.GetCounter().GetValue()) assert.Equalf(t, expectedValue, actualValue, "Got %v, wanted %v as the count while setting label_a to %v and label b to %v", actualValue, expectedValue, aValue, bValue) } } }) } } func TestCounterWithExemplar(t *testing.T) { // Set exemplar. fn := func(offset int) []byte { arr := make([]byte, 16) for i := 0; i < 16; i++ { arr[i] = byte(2<<7 - i - offset) } return arr } traceID := trace.TraceID(fn(1)) spanID := trace.SpanID(fn(2)) ctxForSpanCtx := trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{ SpanID: spanID, TraceID: traceID, TraceFlags: trace.FlagsSampled, })) toAdd := float64(40) // Create contextual counter. counter := NewCounter(&CounterOpts{ Name: "metric_exemplar_test", Help: "helpless", }) _ = counter.WithContext(ctxForSpanCtx) // Register counter. registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) registry.MustRegister(counter) // Call underlying exemplar methods. counter.Add(toAdd) counter.Inc() counter.Inc() // Gather. mfs, err := registry.Gather() if err != nil { t.Fatalf("Gather failed %v", err) } if len(mfs) != 1 { t.Fatalf("Got %v metric families, Want: 1 metric family", len(mfs)) } // Verify metric type. mf := mfs[0] var m *dto.Metric switch mf.GetType() { case dto.MetricType_COUNTER: m = mfs[0].GetMetric()[0] default: t.Fatalf("Got %v metric type, Want: %v metric type", mf.GetType(), dto.MetricType_COUNTER) } // Verify value. want := toAdd + 2 got := m.GetCounter().GetValue() if got != want { t.Fatalf("Got %f, wanted %f as the count", got, want) } // Verify exemplars. e := m.GetCounter().GetExemplar() if e == nil { t.Fatalf("Got nil exemplar, wanted an exemplar") } eLabels := e.GetLabel() if eLabels == nil { t.Fatalf("Got nil exemplar label, wanted an exemplar label") } if len(eLabels) != 2 { t.Fatalf("Got %v exemplar labels, wanted 2 exemplar labels", len(eLabels)) } for _, l := range eLabels { switch *l.Name { case "trace_id": if *l.Value != traceID.String() { t.Fatalf("Got %s as traceID, wanted %s", *l.Value, traceID.String()) } case "span_id": if *l.Value != spanID.String() { t.Fatalf("Got %s as spanID, wanted %s", *l.Value, spanID.String()) } default: t.Fatalf("Got unexpected label %s", *l.Name) } } } kubernetes-component-base-1b2882b/metrics/desc.go000066400000000000000000000153411476422213000220430ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "fmt" "sync" "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" "k8s.io/klog/v2" ) // Desc is a prometheus.Desc extension. // // Use NewDesc to create new Desc instances. type Desc struct { // fqName has been built from Namespace, Subsystem, and Name. fqName string // help provides some helpful information about this metric. help string // constLabels is the label names. Their label values are variable. constLabels Labels // variableLabels contains names of labels for which the metric // maintains variable values. variableLabels []string // promDesc is the descriptor used by every Prometheus Metric. promDesc *prometheus.Desc annotatedHelp string // stabilityLevel represents the API guarantees for a given defined metric. stabilityLevel StabilityLevel // deprecatedVersion represents in which version this metric be deprecated. deprecatedVersion string isDeprecated bool isHidden bool isCreated bool createLock sync.RWMutex markDeprecationOnce sync.Once createOnce sync.Once deprecateOnce sync.Once hideOnce sync.Once annotateOnce sync.Once } // NewDesc extends prometheus.NewDesc with stability support. // // The stabilityLevel should be valid stability label, such as "metrics.ALPHA" // and "metrics.STABLE"(Maybe "metrics.BETA" in future). Default value "metrics.ALPHA" // will be used in case of empty or invalid stability label. // // The deprecatedVersion represents in which version this Metric be deprecated. // The deprecation policy outlined by the control plane metrics stability KEP. func NewDesc(fqName string, help string, variableLabels []string, constLabels Labels, stabilityLevel StabilityLevel, deprecatedVersion string) *Desc { d := &Desc{ fqName: fqName, help: help, annotatedHelp: help, variableLabels: variableLabels, constLabels: constLabels, stabilityLevel: stabilityLevel, deprecatedVersion: deprecatedVersion, } d.stabilityLevel.setDefaults() return d } // String formats the Desc as a string. // The stability metadata maybe annotated in 'HELP' section if called after registry, // otherwise not. // e.g. "Desc{fqName: "normal_stable_descriptor", help: "[STABLE] this is a stable descriptor", constLabels: {}, variableLabels: []}" func (d *Desc) String() string { if d.isCreated { return d.promDesc.String() } return prometheus.NewDesc(d.fqName, d.help, d.variableLabels, prometheus.Labels(d.constLabels)).String() } // toPrometheusDesc transform self to prometheus.Desc func (d *Desc) toPrometheusDesc() *prometheus.Desc { return d.promDesc } // DeprecatedVersion returns a pointer to the Version or nil func (d *Desc) DeprecatedVersion() *semver.Version { return parseSemver(d.deprecatedVersion) } func (d *Desc) determineDeprecationStatus(version semver.Version) { selfVersion := d.DeprecatedVersion() if selfVersion == nil { return } d.markDeprecationOnce.Do(func() { if selfVersion.LTE(version) { d.isDeprecated = true } if ShouldShowHidden() { klog.Warningf("Hidden metrics(%s) have been manually overridden, showing this very deprecated metric.", d.fqName) return } if shouldHide(&version, selfVersion) { // TODO(RainbowMango): Remove this log temporarily. https://github.com/kubernetes/kubernetes/issues/85369 // klog.Warningf("This metric(%s) has been deprecated for more than one release, hiding.", d.fqName) d.isHidden = true } }) } // IsHidden returns if metric will be hidden func (d *Desc) IsHidden() bool { return d.isHidden } // IsDeprecated returns if metric has been deprecated func (d *Desc) IsDeprecated() bool { return d.isDeprecated } // IsCreated returns if metric has been created. func (d *Desc) IsCreated() bool { d.createLock.RLock() defer d.createLock.RUnlock() return d.isCreated } // create forces the initialization of Desc which has been deferred until // the point at which this method is invoked. This method will determine whether // the Desc is deprecated or hidden, no-opting if the Desc should be considered // hidden. Furthermore, this function no-opts and returns true if Desc is already // created. func (d *Desc) create(version *semver.Version) bool { if version != nil { d.determineDeprecationStatus(*version) } // let's not create if this metric is slated to be hidden if d.IsHidden() { return false } d.createOnce.Do(func() { d.createLock.Lock() defer d.createLock.Unlock() d.isCreated = true if d.IsDeprecated() { d.initializeDeprecatedDesc() } else { d.initialize() } }) return d.IsCreated() } // ClearState will clear all the states marked by Create. // It intends to be used for re-register a hidden metric. func (d *Desc) ClearState() { d.isDeprecated = false d.isHidden = false d.isCreated = false d.markDeprecationOnce = *new(sync.Once) d.createOnce = *new(sync.Once) d.deprecateOnce = *new(sync.Once) d.hideOnce = *new(sync.Once) d.annotateOnce = *new(sync.Once) d.annotatedHelp = d.help d.promDesc = nil } func (d *Desc) markDeprecated() { d.deprecateOnce.Do(func() { d.annotatedHelp = fmt.Sprintf("(Deprecated since %s) %s", d.deprecatedVersion, d.annotatedHelp) }) } func (d *Desc) annotateStabilityLevel() { d.annotateOnce.Do(func() { d.annotatedHelp = fmt.Sprintf("[%v] %v", d.stabilityLevel, d.annotatedHelp) }) } func (d *Desc) initialize() { d.annotateStabilityLevel() // this actually creates the underlying prometheus desc. d.promDesc = prometheus.NewDesc(d.fqName, d.annotatedHelp, d.variableLabels, prometheus.Labels(d.constLabels)) } func (d *Desc) initializeDeprecatedDesc() { d.markDeprecated() d.initialize() } // GetRawDesc will returns a new *Desc with original parameters provided to NewDesc(). // // It will be useful in testing scenario that the same Desc be registered to different registry. // 1. Desc `D` is registered to registry 'A' in TestA (Note: `D` maybe created) // 2. Desc `D` is registered to registry 'B' in TestB (Note: since 'D' has been created once, thus will be ignored by registry 'B') func (d *Desc) GetRawDesc() *Desc { return NewDesc(d.fqName, d.help, d.variableLabels, d.constLabels, d.stabilityLevel, d.deprecatedVersion) } kubernetes-component-base-1b2882b/metrics/desc_test.go000066400000000000000000000115761476422213000231100ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "reflect" "strings" "testing" "k8s.io/apimachinery/pkg/version" ) func TestDescCreate(t *testing.T) { currentVersion := parseVersion(version.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.0-alpha-1.12345", }) var tests = []struct { name string fqName string help string stabilityLevel StabilityLevel deprecatedVersion string shouldCreate bool expectedAnnotatedHelp string }{ { name: "alpha descriptor should be created", fqName: "normal_alpha_descriptor", help: "this is an alpha descriptor", stabilityLevel: ALPHA, deprecatedVersion: "", shouldCreate: true, expectedAnnotatedHelp: "[ALPHA] this is an alpha descriptor", }, { name: "stable descriptor should be created", fqName: "normal_stable_descriptor", help: "this is a stable descriptor", stabilityLevel: STABLE, deprecatedVersion: "", shouldCreate: true, expectedAnnotatedHelp: "[STABLE] this is a stable descriptor", }, { name: "deprecated descriptor should be created", fqName: "deprecated_stable_descriptor", help: "this is a deprecated descriptor", stabilityLevel: STABLE, deprecatedVersion: "1.17.0", shouldCreate: true, expectedAnnotatedHelp: "[STABLE] (Deprecated since 1.17.0) this is a deprecated descriptor", }, { name: "hidden descriptor should not be created", fqName: "hidden_stable_descriptor", help: "this is a hidden descriptor", stabilityLevel: STABLE, deprecatedVersion: "1.16.0", shouldCreate: false, expectedAnnotatedHelp: "this is a hidden descriptor", // hidden descriptor shall not be annotated. }, } for _, test := range tests { tc := test t.Run(tc.name, func(t *testing.T) { desc := NewDesc(tc.fqName, tc.help, nil, nil, tc.stabilityLevel, tc.deprecatedVersion) if desc.IsCreated() { t.Fatal("Descriptor should not be created by default.") } desc.create(¤tVersion) desc.create(¤tVersion) // we can safely create a descriptor over and over again. if desc.IsCreated() != tc.shouldCreate { t.Fatalf("expected create state: %v, but got: %v", tc.shouldCreate, desc.IsCreated()) } if !strings.Contains(desc.String(), tc.expectedAnnotatedHelp) { t.Fatalf("expected annotated help: %s, but not in descriptor: %s", tc.expectedAnnotatedHelp, desc.String()) } }) } } func TestDescClearState(t *testing.T) { currentVersion := parseVersion(version.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.0-alpha-1.12345", }) var tests = []struct { name string fqName string help string stabilityLevel StabilityLevel deprecatedVersion string }{ { name: "alpha descriptor", fqName: "normal_alpha_descriptor", help: "this is an alpha descriptor", stabilityLevel: ALPHA, deprecatedVersion: "", }, { name: "stable descriptor", fqName: "normal_stable_descriptor", help: "this is a stable descriptor", stabilityLevel: STABLE, deprecatedVersion: "", }, { name: "deprecated descriptor", fqName: "deprecated_stable_descriptor", help: "this is a deprecated descriptor", stabilityLevel: STABLE, deprecatedVersion: "1.17.0", }, { name: "hidden descriptor", fqName: "hidden_stable_descriptor", help: "this is a hidden descriptor", stabilityLevel: STABLE, deprecatedVersion: "1.16.0", }, } for _, test := range tests { tc := test t.Run(tc.name, func(t *testing.T) { descA := NewDesc(tc.fqName, tc.help, nil, nil, tc.stabilityLevel, tc.deprecatedVersion) descB := NewDesc(tc.fqName, tc.help, nil, nil, tc.stabilityLevel, tc.deprecatedVersion) descA.create(¤tVersion) descA.ClearState() // create //nolint:govet // it's okay to compare sync.RWMutex, it's empty if !reflect.DeepEqual(*descA, *descB) { t.Fatal("descriptor state hasn't be cleaned up") } }) } } kubernetes-component-base-1b2882b/metrics/features/000077500000000000000000000000001476422213000224105ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/features/kube_features.go000066400000000000000000000031141476422213000255620ustar00rootroot00000000000000/* Copyright 2022 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 features import ( "k8s.io/apimachinery/pkg/util/version" "k8s.io/component-base/featuregate" ) const ( // owner: @logicalhan // kep: https://kep.k8s.io/3466 ComponentSLIs featuregate.Feature = "ComponentSLIs" ) func featureGates() map[featuregate.Feature]featuregate.VersionedSpecs { return map[featuregate.Feature]featuregate.VersionedSpecs{ ComponentSLIs: { {Version: version.MustParse("1.26"), Default: false, PreRelease: featuregate.Alpha}, {Version: version.MustParse("1.27"), Default: true, PreRelease: featuregate.Beta}, // ComponentSLIs officially graduated to GA in v1.29 but the gate was not updated until v1.32. // To support emulated versions, keep the gate until v1.35. {Version: version.MustParse("1.32"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, }, } } // AddFeatureGates adds all feature gates used by this package. func AddFeatureGates(mutableFeatureGate featuregate.MutableVersionedFeatureGate) error { return mutableFeatureGate.AddVersioned(featureGates()) } kubernetes-component-base-1b2882b/metrics/gauge.go000066400000000000000000000234101476422213000222110ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "context" "sync" "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" "k8s.io/component-base/version" ) // Gauge is our internal representation for our wrapping struct around prometheus // gauges. kubeGauge implements both kubeCollector and KubeGauge. type Gauge struct { GaugeMetric *GaugeOpts lazyMetric selfCollector } var _ GaugeMetric = &Gauge{} var _ Registerable = &Gauge{} var _ kubeCollector = &Gauge{} // NewGauge returns an object which satisfies the kubeCollector, Registerable, and Gauge interfaces. // However, the object returned will not measure anything unless the collector is first // registered, since the metric is lazily instantiated. func NewGauge(opts *GaugeOpts) *Gauge { opts.StabilityLevel.setDefaults() kc := &Gauge{ GaugeOpts: opts, lazyMetric: lazyMetric{stabilityLevel: opts.StabilityLevel}, } kc.setPrometheusGauge(noop) kc.lazyInit(kc, BuildFQName(opts.Namespace, opts.Subsystem, opts.Name)) return kc } // setPrometheusGauge sets the underlying KubeGauge object, i.e. the thing that does the measurement. func (g *Gauge) setPrometheusGauge(gauge prometheus.Gauge) { g.GaugeMetric = gauge g.initSelfCollection(gauge) } // DeprecatedVersion returns a pointer to the Version or nil func (g *Gauge) DeprecatedVersion() *semver.Version { return parseSemver(g.GaugeOpts.DeprecatedVersion) } // initializeMetric invocation creates the actual underlying Gauge. Until this method is called // the underlying gauge is a no-op. func (g *Gauge) initializeMetric() { g.GaugeOpts.annotateStabilityLevel() // this actually creates the underlying prometheus gauge. g.setPrometheusGauge(prometheus.NewGauge(g.GaugeOpts.toPromGaugeOpts())) } // initializeDeprecatedMetric invocation creates the actual (but deprecated) Gauge. Until this method // is called the underlying gauge is a no-op. func (g *Gauge) initializeDeprecatedMetric() { g.GaugeOpts.markDeprecated() g.initializeMetric() } // WithContext allows the normal Gauge metric to pass in context. The context is no-op now. func (g *Gauge) WithContext(ctx context.Context) GaugeMetric { return g.GaugeMetric } // GaugeVec is the internal representation of our wrapping struct around prometheus // gaugeVecs. kubeGaugeVec implements both kubeCollector and KubeGaugeVec. type GaugeVec struct { *prometheus.GaugeVec *GaugeOpts lazyMetric originalLabels []string } var _ GaugeVecMetric = &GaugeVec{} var _ Registerable = &GaugeVec{} var _ kubeCollector = &GaugeVec{} // NewGaugeVec returns an object which satisfies the kubeCollector, Registerable, and GaugeVecMetric interfaces. // However, the object returned will not measure anything unless the collector is first // registered, since the metric is lazily instantiated, and only members extracted after // registration will actually measure anything. func NewGaugeVec(opts *GaugeOpts, labels []string) *GaugeVec { opts.StabilityLevel.setDefaults() fqName := BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) cv := &GaugeVec{ GaugeVec: noopGaugeVec, GaugeOpts: opts, originalLabels: labels, lazyMetric: lazyMetric{stabilityLevel: opts.StabilityLevel}, } cv.lazyInit(cv, fqName) return cv } // DeprecatedVersion returns a pointer to the Version or nil func (v *GaugeVec) DeprecatedVersion() *semver.Version { return parseSemver(v.GaugeOpts.DeprecatedVersion) } // initializeMetric invocation creates the actual underlying GaugeVec. Until this method is called // the underlying gaugeVec is a no-op. func (v *GaugeVec) initializeMetric() { v.GaugeOpts.annotateStabilityLevel() v.GaugeVec = prometheus.NewGaugeVec(v.GaugeOpts.toPromGaugeOpts(), v.originalLabels) } // initializeDeprecatedMetric invocation creates the actual (but deprecated) GaugeVec. Until this method is called // the underlying gaugeVec is a no-op. func (v *GaugeVec) initializeDeprecatedMetric() { v.GaugeOpts.markDeprecated() v.initializeMetric() } func (v *GaugeVec) WithLabelValuesChecked(lvs ...string) (GaugeMetric, error) { if !v.IsCreated() { if v.IsHidden() { return noop, nil } return noop, errNotRegistered // return no-op gauge } if v.LabelValueAllowLists != nil { v.LabelValueAllowLists.ConstrainToAllowedList(v.originalLabels, lvs) } else { v.initializeLabelAllowListsOnce.Do(func() { allowListLock.RLock() if allowList, ok := labelValueAllowLists[v.FQName()]; ok { v.LabelValueAllowLists = allowList allowList.ConstrainToAllowedList(v.originalLabels, lvs) } allowListLock.RUnlock() }) } elt, err := v.GaugeVec.GetMetricWithLabelValues(lvs...) return elt, err } // Default Prometheus Vec behavior is that member extraction results in creation of a new element // if one with the unique label values is not found in the underlying stored metricMap. // This means that if this function is called but the underlying metric is not registered // (which means it will never be exposed externally nor consumed), the metric will exist in memory // for perpetuity (i.e. throughout application lifecycle). // // For reference: https://github.com/prometheus/client_golang/blob/v0.9.2/prometheus/gauge.go#L190-L208 // // In contrast, the Vec behavior in this package is that member extraction before registration // returns a permanent noop object. // WithLabelValues returns the GaugeMetric for the given slice of label // values (same order as the VariableLabels in Desc). If that combination of // label values is accessed for the first time, a new GaugeMetric is created IFF the gaugeVec // has been registered to a metrics registry. func (v *GaugeVec) WithLabelValues(lvs ...string) GaugeMetric { ans, err := v.WithLabelValuesChecked(lvs...) if err == nil || ErrIsNotRegistered(err) { return ans } panic(err) } func (v *GaugeVec) WithChecked(labels map[string]string) (GaugeMetric, error) { if !v.IsCreated() { if v.IsHidden() { return noop, nil } return noop, errNotRegistered // return no-op gauge } if v.LabelValueAllowLists != nil { v.LabelValueAllowLists.ConstrainLabelMap(labels) } else { v.initializeLabelAllowListsOnce.Do(func() { allowListLock.RLock() if allowList, ok := labelValueAllowLists[v.FQName()]; ok { v.LabelValueAllowLists = allowList allowList.ConstrainLabelMap(labels) } allowListLock.RUnlock() }) } elt, err := v.GaugeVec.GetMetricWith(labels) return elt, err } // With returns the GaugeMetric for the given Labels map (the label names // must match those of the VariableLabels in Desc). If that label map is // accessed for the first time, a new GaugeMetric is created IFF the gaugeVec has // been registered to a metrics registry. func (v *GaugeVec) With(labels map[string]string) GaugeMetric { ans, err := v.WithChecked(labels) if err == nil || ErrIsNotRegistered(err) { return ans } panic(err) } // Delete deletes the metric where the variable labels are the same as those // passed in as labels. It returns true if a metric was deleted. // // It is not an error if the number and names of the Labels are inconsistent // with those of the VariableLabels in Desc. However, such inconsistent Labels // can never match an actual metric, so the method will always return false in // that case. func (v *GaugeVec) Delete(labels map[string]string) bool { if !v.IsCreated() { return false // since we haven't created the metric, we haven't deleted a metric with the passed in values } return v.GaugeVec.Delete(labels) } // Reset deletes all metrics in this vector. func (v *GaugeVec) Reset() { if !v.IsCreated() { return } v.GaugeVec.Reset() } // ResetLabelAllowLists resets the label allow list for the GaugeVec. // NOTE: This should only be used in test. func (v *GaugeVec) ResetLabelAllowLists() { v.initializeLabelAllowListsOnce = sync.Once{} v.LabelValueAllowLists = nil } func newGaugeFunc(opts *GaugeOpts, function func() float64, v semver.Version) GaugeFunc { g := NewGauge(opts) if !g.Create(&v) { return nil } return prometheus.NewGaugeFunc(g.GaugeOpts.toPromGaugeOpts(), function) } // NewGaugeFunc creates a new GaugeFunc based on the provided GaugeOpts. The // value reported is determined by calling the given function from within the // Write method. Take into account that metric collection may happen // concurrently. If that results in concurrent calls to Write, like in the case // where a GaugeFunc is directly registered with Prometheus, the provided // function must be concurrency-safe. func NewGaugeFunc(opts *GaugeOpts, function func() float64) GaugeFunc { v := parseVersion(version.Get()) return newGaugeFunc(opts, function, v) } // WithContext returns wrapped GaugeVec with context func (v *GaugeVec) WithContext(ctx context.Context) *GaugeVecWithContext { return &GaugeVecWithContext{ ctx: ctx, GaugeVec: v, } } func (v *GaugeVec) InterfaceWithContext(ctx context.Context) GaugeVecMetric { return v.WithContext(ctx) } // GaugeVecWithContext is the wrapper of GaugeVec with context. type GaugeVecWithContext struct { *GaugeVec ctx context.Context } // WithLabelValues is the wrapper of GaugeVec.WithLabelValues. func (vc *GaugeVecWithContext) WithLabelValues(lvs ...string) GaugeMetric { return vc.GaugeVec.WithLabelValues(lvs...) } // With is the wrapper of GaugeVec.With. func (vc *GaugeVecWithContext) With(labels map[string]string) GaugeMetric { return vc.GaugeVec.With(labels) } kubernetes-component-base-1b2882b/metrics/gauge_test.go000066400000000000000000000234731476422213000232610ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "strings" "testing" "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" apimachineryversion "k8s.io/apimachinery/pkg/version" ) func TestGauge(t *testing.T) { v115 := semver.MustParse("1.15.0") var tests = []struct { desc string *GaugeOpts registryVersion *semver.Version expectedMetricCount int expectedHelp string }{ { desc: "Test non deprecated", GaugeOpts: &GaugeOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "gauge help", }, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "[ALPHA] gauge help", }, { desc: "Test deprecated", GaugeOpts: &GaugeOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "gauge help", DeprecatedVersion: "1.15.0", }, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "[ALPHA] (Deprecated since 1.15.0) gauge help", }, { desc: "Test hidden", GaugeOpts: &GaugeOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "gauge help", DeprecatedVersion: "1.14.0", }, registryVersion: &v115, expectedMetricCount: 0, expectedHelp: "gauge help", }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) c := NewGauge(test.GaugeOpts) registry.MustRegister(c) ms, err := registry.Gather() assert.Lenf(t, ms, test.expectedMetricCount, "Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) require.NoError(t, err, "Gather failed %v", err) for _, metric := range ms { assert.Equalf(t, test.expectedHelp, metric.GetHelp(), "Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) } // let's increment the counter and verify that the metric still works c.Set(100) c.Set(101) expected := 101 ms, err = registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range ms { for _, m := range mf.GetMetric() { assert.Equalf(t, expected, int(m.GetGauge().GetValue()), "Got %v, wanted %v as the count", m.GetGauge().GetValue(), expected) t.Logf("%v\n", m.GetGauge().GetValue()) } } }) } } func TestGaugeVec(t *testing.T) { v115 := semver.MustParse("1.15.0") var tests = []struct { desc string *GaugeOpts labels []string registryVersion *semver.Version expectedMetricCount int expectedHelp string }{ { desc: "Test non deprecated", GaugeOpts: &GaugeOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "gauge help", }, labels: []string{"label_a", "label_b"}, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "[ALPHA] gauge help", }, { desc: "Test deprecated", GaugeOpts: &GaugeOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "gauge help", DeprecatedVersion: "1.15.0", }, labels: []string{"label_a", "label_b"}, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "[ALPHA] (Deprecated since 1.15.0) gauge help", }, { desc: "Test hidden", GaugeOpts: &GaugeOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "gauge help", DeprecatedVersion: "1.14.0", }, labels: []string{"label_a", "label_b"}, registryVersion: &v115, expectedMetricCount: 0, expectedHelp: "gauge help", }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) c := NewGaugeVec(test.GaugeOpts, test.labels) registry.MustRegister(c) c.WithLabelValues("1", "2").Set(1.0) ms, err := registry.Gather() assert.Lenf(t, ms, test.expectedMetricCount, "Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) require.NoError(t, err, "Gather failed %v", err) for _, metric := range ms { assert.Equalf(t, test.expectedHelp, metric.GetHelp(), "Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) } // let's increment the counter and verify that the metric still works c.WithLabelValues("1", "3").Set(1.0) c.WithLabelValues("2", "3").Set(1.0) ms, err = registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range ms { assert.Lenf(t, mf.GetMetric(), 3, "Got %v metrics, wanted 3 as the count", len(mf.GetMetric())) } }) } } func TestGaugeFunc(t *testing.T) { currentVersion := apimachineryversion.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.0-alpha-1.12345", } var function = func() float64 { return 1 } var tests = []struct { desc string *GaugeOpts expectedMetrics string }{ { desc: "Test non deprecated", GaugeOpts: &GaugeOpts{ Namespace: "namespace", Subsystem: "subsystem", Name: "metric_non_deprecated", Help: "gauge help", }, expectedMetrics: ` # HELP namespace_subsystem_metric_non_deprecated [ALPHA] gauge help # TYPE namespace_subsystem_metric_non_deprecated gauge namespace_subsystem_metric_non_deprecated 1 `, }, { desc: "Test deprecated", GaugeOpts: &GaugeOpts{ Namespace: "namespace", Subsystem: "subsystem", Name: "metric_deprecated", Help: "gauge help", DeprecatedVersion: "1.17.0", }, expectedMetrics: ` # HELP namespace_subsystem_metric_deprecated [ALPHA] (Deprecated since 1.17.0) gauge help # TYPE namespace_subsystem_metric_deprecated gauge namespace_subsystem_metric_deprecated 1 `, }, { desc: "Test hidden", GaugeOpts: &GaugeOpts{ Namespace: "namespace", Subsystem: "subsystem", Name: "metric_hidden", Help: "gauge help", DeprecatedVersion: "1.16.0", }, expectedMetrics: "", }, } for _, test := range tests { tc := test t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(currentVersion) gauge := newGaugeFunc(tc.GaugeOpts, function, parseVersion(currentVersion)) if gauge != nil { // hidden metrics will not be initialize, register is not allowed registry.RawMustRegister(gauge) } metricName := BuildFQName(tc.GaugeOpts.Namespace, tc.GaugeOpts.Subsystem, tc.GaugeOpts.Name) if err := testutil.GatherAndCompare(registry, strings.NewReader(tc.expectedMetrics), metricName); err != nil { t.Fatal(err) } }) } } func TestGaugeWithLabelValueAllowList(t *testing.T) { labelAllowValues := map[string]string{ "namespace_subsystem_metric_allowlist_test,label_a": "allowed", } labels := []string{"label_a", "label_b"} opts := &GaugeOpts{ Namespace: "namespace", Name: "metric_allowlist_test", Subsystem: "subsystem", } var tests = []struct { desc string labelValues [][]string expectMetricValues map[string]float64 }{ { desc: "Test no unexpected input", labelValues: [][]string{{"allowed", "b1"}, {"allowed", "b2"}}, expectMetricValues: map[string]float64{ "allowed b1": 100.0, "allowed b2": 100.0, }, }, { desc: "Test unexpected input", labelValues: [][]string{{"allowed", "b1"}, {"not_allowed", "b1"}}, expectMetricValues: map[string]float64{ "allowed b1": 100.0, "unexpected b1": 100.0, }, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { labelValueAllowLists = map[string]*MetricLabelAllowList{} registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) g := NewGaugeVec(opts, labels) registry.MustRegister(g) SetLabelAllowListFromCLI(labelAllowValues) for _, lv := range test.labelValues { g.WithLabelValues(lv...).Set(100.0) } mfs, err := registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range mfs { if *mf.Name != BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) { continue } mfMetric := mf.GetMetric() for _, m := range mfMetric { var aValue, bValue string for _, l := range m.Label { if *l.Name == "label_a" { aValue = *l.Value } if *l.Name == "label_b" { bValue = *l.Value } } labelValuePair := aValue + " " + bValue expectedValue, ok := test.expectMetricValues[labelValuePair] assert.True(t, ok, "Got unexpected label values, lable_a is %v, label_b is %v", aValue, bValue) actualValue := m.GetGauge().GetValue() assert.InDeltaf(t, expectedValue, actualValue, 0.01, "Got %v, wanted %v as the gauge while setting label_a to %v and label b to %v", actualValue, expectedValue, aValue, bValue) } } }) } } kubernetes-component-base-1b2882b/metrics/histogram.go000066400000000000000000000234171476422213000231250ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "context" "sync" "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/trace" ) // Histogram is our internal representation for our wrapping struct around prometheus // histograms. Summary implements both kubeCollector and ObserverMetric type Histogram struct { ctx context.Context ObserverMetric *HistogramOpts lazyMetric selfCollector } // exemplarHistogramMetric holds a context to extract exemplar labels from, and a historgram metric to attach them to. It implements the metricWithExemplar interface. type exemplarHistogramMetric struct { *Histogram } type exemplarHistogramVec struct { *HistogramVecWithContext observer prometheus.Observer } func (h *Histogram) Observe(v float64) { h.withExemplar(v) } // withExemplar initializes the exemplarMetric object and sets the exemplar value. func (h *Histogram) withExemplar(v float64) { (&exemplarHistogramMetric{h}).withExemplar(v) } // withExemplar attaches an exemplar to the metric. func (e *exemplarHistogramMetric) withExemplar(v float64) { if m, ok := e.Histogram.ObserverMetric.(prometheus.ExemplarObserver); ok { maybeSpanCtx := trace.SpanContextFromContext(e.ctx) if maybeSpanCtx.IsValid() && maybeSpanCtx.IsSampled() { exemplarLabels := prometheus.Labels{ "trace_id": maybeSpanCtx.TraceID().String(), "span_id": maybeSpanCtx.SpanID().String(), } m.ObserveWithExemplar(v, exemplarLabels) return } } e.ObserverMetric.Observe(v) } // NewHistogram returns an object which is Histogram-like. However, nothing // will be measured until the histogram is registered somewhere. func NewHistogram(opts *HistogramOpts) *Histogram { opts.StabilityLevel.setDefaults() h := &Histogram{ HistogramOpts: opts, lazyMetric: lazyMetric{stabilityLevel: opts.StabilityLevel}, } h.setPrometheusHistogram(noopMetric{}) h.lazyInit(h, BuildFQName(opts.Namespace, opts.Subsystem, opts.Name)) return h } // setPrometheusHistogram sets the underlying KubeGauge object, i.e. the thing that does the measurement. func (h *Histogram) setPrometheusHistogram(histogram prometheus.Histogram) { h.ObserverMetric = histogram h.initSelfCollection(histogram) } // DeprecatedVersion returns a pointer to the Version or nil func (h *Histogram) DeprecatedVersion() *semver.Version { return parseSemver(h.HistogramOpts.DeprecatedVersion) } // initializeMetric invokes the actual prometheus.Histogram object instantiation // and stores a reference to it func (h *Histogram) initializeMetric() { h.HistogramOpts.annotateStabilityLevel() // this actually creates the underlying prometheus gauge. h.setPrometheusHistogram(prometheus.NewHistogram(h.HistogramOpts.toPromHistogramOpts())) } // initializeDeprecatedMetric invokes the actual prometheus.Histogram object instantiation // but modifies the Help description prior to object instantiation. func (h *Histogram) initializeDeprecatedMetric() { h.HistogramOpts.markDeprecated() h.initializeMetric() } // WithContext allows the normal Histogram metric to pass in context. The context is no-op now. func (h *Histogram) WithContext(ctx context.Context) ObserverMetric { h.ctx = ctx return h.ObserverMetric } // HistogramVec is the internal representation of our wrapping struct around prometheus // histogramVecs. type HistogramVec struct { *prometheus.HistogramVec *HistogramOpts lazyMetric originalLabels []string } // NewHistogramVec returns an object which satisfies kubeCollector and wraps the // prometheus.HistogramVec object. However, the object returned will not measure // anything unless the collector is first registered, since the metric is lazily instantiated, // and only members extracted after // registration will actually measure anything. func NewHistogramVec(opts *HistogramOpts, labels []string) *HistogramVec { opts.StabilityLevel.setDefaults() fqName := BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) v := &HistogramVec{ HistogramVec: noopHistogramVec, HistogramOpts: opts, originalLabels: labels, lazyMetric: lazyMetric{stabilityLevel: opts.StabilityLevel}, } v.lazyInit(v, fqName) return v } // DeprecatedVersion returns a pointer to the Version or nil func (v *HistogramVec) DeprecatedVersion() *semver.Version { return parseSemver(v.HistogramOpts.DeprecatedVersion) } func (v *HistogramVec) initializeMetric() { v.HistogramOpts.annotateStabilityLevel() v.HistogramVec = prometheus.NewHistogramVec(v.HistogramOpts.toPromHistogramOpts(), v.originalLabels) } func (v *HistogramVec) initializeDeprecatedMetric() { v.HistogramOpts.markDeprecated() v.initializeMetric() } // Default Prometheus Vec behavior is that member extraction results in creation of a new element // if one with the unique label values is not found in the underlying stored metricMap. // This means that if this function is called but the underlying metric is not registered // (which means it will never be exposed externally nor consumed), the metric will exist in memory // for perpetuity (i.e. throughout application lifecycle). // // For reference: https://github.com/prometheus/client_golang/blob/v0.9.2/prometheus/histogram.go#L460-L470 // // In contrast, the Vec behavior in this package is that member extraction before registration // returns a permanent noop object. // WithLabelValues returns the ObserverMetric for the given slice of label // values (same order as the VariableLabels in Desc). If that combination of // label values is accessed for the first time, a new ObserverMetric is created IFF the HistogramVec // has been registered to a metrics registry. func (v *HistogramVec) WithLabelValues(lvs ...string) ObserverMetric { if !v.IsCreated() { return noop } if v.LabelValueAllowLists != nil { v.LabelValueAllowLists.ConstrainToAllowedList(v.originalLabels, lvs) } else { v.initializeLabelAllowListsOnce.Do(func() { allowListLock.RLock() if allowList, ok := labelValueAllowLists[v.FQName()]; ok { v.LabelValueAllowLists = allowList allowList.ConstrainToAllowedList(v.originalLabels, lvs) } allowListLock.RUnlock() }) } return v.HistogramVec.WithLabelValues(lvs...) } // With returns the ObserverMetric for the given Labels map (the label names // must match those of the VariableLabels in Desc). If that label map is // accessed for the first time, a new ObserverMetric is created IFF the HistogramVec has // been registered to a metrics registry. func (v *HistogramVec) With(labels map[string]string) ObserverMetric { if !v.IsCreated() { return noop } if v.LabelValueAllowLists != nil { v.LabelValueAllowLists.ConstrainLabelMap(labels) } else { v.initializeLabelAllowListsOnce.Do(func() { allowListLock.RLock() if allowList, ok := labelValueAllowLists[v.FQName()]; ok { v.LabelValueAllowLists = allowList allowList.ConstrainLabelMap(labels) } allowListLock.RUnlock() }) } return v.HistogramVec.With(labels) } // Delete deletes the metric where the variable labels are the same as those // passed in as labels. It returns true if a metric was deleted. // // It is not an error if the number and names of the Labels are inconsistent // with those of the VariableLabels in Desc. However, such inconsistent Labels // can never match an actual metric, so the method will always return false in // that case. func (v *HistogramVec) Delete(labels map[string]string) bool { if !v.IsCreated() { return false // since we haven't created the metric, we haven't deleted a metric with the passed in values } return v.HistogramVec.Delete(labels) } // Reset deletes all metrics in this vector. func (v *HistogramVec) Reset() { if !v.IsCreated() { return } v.HistogramVec.Reset() } // ResetLabelAllowLists resets the label allow list for the HistogramVec. // NOTE: This should only be used in test. func (v *HistogramVec) ResetLabelAllowLists() { v.initializeLabelAllowListsOnce = sync.Once{} v.LabelValueAllowLists = nil } // WithContext returns wrapped HistogramVec with context func (v *HistogramVec) WithContext(ctx context.Context) *HistogramVecWithContext { return &HistogramVecWithContext{ ctx: ctx, HistogramVec: v, } } // HistogramVecWithContext is the wrapper of HistogramVec with context. type HistogramVecWithContext struct { *HistogramVec ctx context.Context } func (h *exemplarHistogramVec) Observe(v float64) { h.withExemplar(v) } func (h *exemplarHistogramVec) withExemplar(v float64) { if m, ok := h.observer.(prometheus.ExemplarObserver); ok { maybeSpanCtx := trace.SpanContextFromContext(h.HistogramVecWithContext.ctx) if maybeSpanCtx.IsValid() && maybeSpanCtx.IsSampled() { m.ObserveWithExemplar(v, prometheus.Labels{ "trace_id": maybeSpanCtx.TraceID().String(), "span_id": maybeSpanCtx.SpanID().String(), }) return } } h.observer.Observe(v) } // WithLabelValues is the wrapper of HistogramVec.WithLabelValues. func (vc *HistogramVecWithContext) WithLabelValues(lvs ...string) *exemplarHistogramVec { return &exemplarHistogramVec{ HistogramVecWithContext: vc, observer: vc.HistogramVec.WithLabelValues(lvs...), } } // With is the wrapper of HistogramVec.With. func (vc *HistogramVecWithContext) With(labels map[string]string) *exemplarHistogramVec { return &exemplarHistogramVec{ HistogramVecWithContext: vc, observer: vc.HistogramVec.With(labels), } } kubernetes-component-base-1b2882b/metrics/histogram_test.go000066400000000000000000000331031476422213000241550ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "context" "testing" "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" apimachineryversion "k8s.io/apimachinery/pkg/version" ) func TestHistogram(t *testing.T) { v115 := semver.MustParse("1.15.0") var tests = []struct { desc string *HistogramOpts registryVersion *semver.Version expectedMetricCount int expectedHelp string }{ { desc: "Test non deprecated", HistogramOpts: &HistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", Buckets: prometheus.DefBuckets, }, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "[ALPHA] histogram help message", }, { desc: "Test deprecated", HistogramOpts: &HistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", DeprecatedVersion: "1.15.0", Buckets: prometheus.DefBuckets, }, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "[ALPHA] (Deprecated since 1.15.0) histogram help message", }, { desc: "Test hidden", HistogramOpts: &HistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", DeprecatedVersion: "1.14.0", Buckets: prometheus.DefBuckets, }, registryVersion: &v115, expectedMetricCount: 0, expectedHelp: "histogram help message", }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) c := NewHistogram(test.HistogramOpts) registry.MustRegister(c) cm := c.ObserverMetric.(prometheus.Metric) metricChan := make(chan prometheus.Metric, 2) c.Collect(metricChan) close(metricChan) m1 := <-metricChan if m1 != cm { t.Error("Unexpected metric", m1, cm) } m2, ok := <-metricChan if ok { t.Error("Unexpected second metric", m2) } ms, err := registry.Gather() assert.Lenf(t, ms, test.expectedMetricCount, "Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) require.NoError(t, err, "Gather failed %v", err) for _, metric := range ms { assert.Equalf(t, test.expectedHelp, metric.GetHelp(), "Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) } // let's increment the counter and verify that the metric still works c.Observe(1) c.Observe(2) c.Observe(3) c.Observe(1.5) expected := 4 ms, err = registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range ms { for _, m := range mf.GetMetric() { assert.Equalf(t, expected, int(m.GetHistogram().GetSampleCount()), "Got %v, want %v as the sample count", m.GetHistogram().GetSampleCount(), expected) } } }) } } func TestHistogramVec(t *testing.T) { v115 := semver.MustParse("1.15.0") var tests = []struct { desc string *HistogramOpts labels []string registryVersion *semver.Version expectedMetricCount int expectedHelp string }{ { desc: "Test non deprecated", HistogramOpts: &HistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", Buckets: prometheus.DefBuckets, }, labels: []string{"label_a", "label_b"}, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "[ALPHA] histogram help message", }, { desc: "Test deprecated", HistogramOpts: &HistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", DeprecatedVersion: "1.15.0", Buckets: prometheus.DefBuckets, }, labels: []string{"label_a", "label_b"}, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "[ALPHA] (Deprecated since 1.15.0) histogram help message", }, { desc: "Test hidden", HistogramOpts: &HistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", DeprecatedVersion: "1.14.0", Buckets: prometheus.DefBuckets, }, labels: []string{"label_a", "label_b"}, registryVersion: &v115, expectedMetricCount: 0, expectedHelp: "histogram help message", }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) c := NewHistogramVec(test.HistogramOpts, test.labels) registry.MustRegister(c) ov12 := c.WithLabelValues("1", "2") cm1 := ov12.(prometheus.Metric) ov12.Observe(1.0) if test.expectedMetricCount > 0 { metricChan := make(chan prometheus.Metric, 2) c.Collect(metricChan) close(metricChan) m1 := <-metricChan if m1 != cm1 { t.Error("Unexpected metric", m1, cm1) } m2, ok := <-metricChan if ok { t.Error("Unexpected second metric", m2) } } ms, err := registry.Gather() assert.Lenf(t, ms, test.expectedMetricCount, "Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) require.NoError(t, err, "Gather failed %v", err) for _, metric := range ms { if metric.GetHelp() != test.expectedHelp { assert.Equalf(t, test.expectedHelp, metric.GetHelp(), "Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) } } // let's increment the counter and verify that the metric still works c.WithLabelValues("1", "3").Observe(1.0) c.WithLabelValues("2", "3").Observe(1.0) ms, err = registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range ms { assert.Lenf(t, mf.GetMetric(), 3, "Got %v metrics, wanted 3 as the count", len(mf.GetMetric())) for _, m := range mf.GetMetric() { assert.Equalf(t, uint64(1), m.GetHistogram().GetSampleCount(), "Got %v metrics, expected histogram sample count to equal 1", m.GetHistogram().GetSampleCount()) } } }) } } func TestHistogramWithLabelValueAllowList(t *testing.T) { labelAllowValues := map[string]string{ "namespace_subsystem_metric_allowlist_test,label_a": "allowed", } labels := []string{"label_a", "label_b"} opts := &HistogramOpts{ Namespace: "namespace", Name: "metric_allowlist_test", Subsystem: "subsystem", } var tests = []struct { desc string labelValues [][]string expectMetricValues map[string]uint64 }{ { desc: "Test no unexpected input", labelValues: [][]string{{"allowed", "b1"}, {"allowed", "b2"}}, expectMetricValues: map[string]uint64{ "allowed b1": 1.0, "allowed b2": 1.0, }, }, { desc: "Test unexpected input", labelValues: [][]string{{"allowed", "b1"}, {"not_allowed", "b1"}}, expectMetricValues: map[string]uint64{ "allowed b1": 1.0, "unexpected b1": 1.0, }, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { labelValueAllowLists = map[string]*MetricLabelAllowList{} registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) c := NewHistogramVec(opts, labels) registry.MustRegister(c) SetLabelAllowListFromCLI(labelAllowValues) for _, lv := range test.labelValues { c.WithLabelValues(lv...).Observe(1.0) } mfs, err := registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range mfs { if *mf.Name != BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) { continue } mfMetric := mf.GetMetric() for _, m := range mfMetric { var aValue, bValue string for _, l := range m.Label { if *l.Name == "label_a" { aValue = *l.Value } if *l.Name == "label_b" { bValue = *l.Value } } labelValuePair := aValue + " " + bValue expectedValue, ok := test.expectMetricValues[labelValuePair] assert.True(t, ok, "Got unexpected label values, lable_a is %v, label_b is %v", aValue, bValue) actualValue := m.GetHistogram().GetSampleCount() assert.Equalf(t, expectedValue, actualValue, "Got %v, wanted %v as the count while setting label_a to %v and label b to %v", actualValue, expectedValue, aValue, bValue) } } }) } } func TestHistogramWithExemplar(t *testing.T) { // Arrange. traceID := trace.TraceID([]byte("trace-0000-xxxxx")) spanID := trace.SpanID([]byte("span-0000-xxxxx")) ctxForSpanCtx := trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{ TraceID: traceID, SpanID: spanID, TraceFlags: trace.FlagsSampled, })) value := float64(10) histogram := NewHistogram(&HistogramOpts{ Name: "histogram_exemplar_test", Help: "helpless", Buckets: []float64{100}, }) _ = histogram.WithContext(ctxForSpanCtx) registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) registry.MustRegister(histogram) // Act. histogram.Observe(value) // Assert. mfs, err := registry.Gather() if err != nil { t.Fatalf("Gather failed %v", err) } if len(mfs) != 1 { t.Fatalf("Got %v metric families, Want: 1 metric family", len(mfs)) } mf := mfs[0] var m *dto.Metric switch mf.GetType() { case dto.MetricType_HISTOGRAM: m = mfs[0].GetMetric()[0] default: t.Fatalf("Got %v metric type, Want: %v metric type", mf.GetType(), dto.MetricType_COUNTER) } want := value got := m.GetHistogram().GetSampleSum() if got != want { t.Fatalf("Got %f, wanted %f as the count", got, want) } buckets := m.GetHistogram().GetBucket() if len(buckets) == 0 { t.Fatalf("Got 0 buckets, wanted 1") } e := buckets[0].GetExemplar() if e == nil { t.Fatalf("Got nil exemplar, wanted an exemplar") } eLabels := e.GetLabel() if eLabels == nil { t.Fatalf("Got nil exemplar label, wanted an exemplar label") } if len(eLabels) != 2 { t.Fatalf("Got %v exemplar labels, wanted 2 exemplar labels", len(eLabels)) } for _, l := range eLabels { switch *l.Name { case "trace_id": if *l.Value != traceID.String() { t.Fatalf("Got %s as traceID, wanted %s", *l.Value, traceID.String()) } case "span_id": if *l.Value != spanID.String() { t.Fatalf("Got %s as spanID, wanted %s", *l.Value, spanID.String()) } default: t.Fatalf("Got unexpected label %s", *l.Name) } } } func TestHistogramVecWithExemplar(t *testing.T) { // Arrange. traceID := trace.TraceID([]byte("trace-0000-xxxxx")) spanID := trace.SpanID([]byte("span-0000-xxxxx")) ctxForSpanCtx := trace.ContextWithSpanContext(context.Background(), trace.NewSpanContext(trace.SpanContextConfig{ TraceID: traceID, SpanID: spanID, TraceFlags: trace.FlagsSampled, })) value := float64(10) histogramVec := NewHistogramVec(&HistogramOpts{ Name: "histogram_exemplar_test", Help: "helpless", Buckets: []float64{100}, }, []string{"group"}) h := histogramVec.WithContext(ctxForSpanCtx) registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) registry.MustRegister(histogramVec) // Act. h.WithLabelValues("foo").Observe(value) // Assert. mfs, err := registry.Gather() if err != nil { t.Fatalf("Gather failed %v", err) } if len(mfs) != 1 { t.Fatalf("Got %v metric families, Want: 1 metric family", len(mfs)) } mf := mfs[0] var m *dto.Metric switch mf.GetType() { case dto.MetricType_HISTOGRAM: m = mfs[0].GetMetric()[0] default: t.Fatalf("Got %v metric type, Want: %v metric type", mf.GetType(), dto.MetricType_COUNTER) } want := value got := m.GetHistogram().GetSampleSum() if got != want { t.Fatalf("Got %f, wanted %f as the count", got, want) } buckets := m.GetHistogram().GetBucket() if len(buckets) == 0 { t.Fatalf("Got 0 buckets, wanted 1") } e := buckets[0].GetExemplar() if e == nil { t.Fatalf("Got nil exemplar, wanted an exemplar") } eLabels := e.GetLabel() if eLabels == nil { t.Fatalf("Got nil exemplar label, wanted an exemplar label") } if len(eLabels) != 2 { t.Fatalf("Got %v exemplar labels, wanted 2 exemplar labels", len(eLabels)) } for _, l := range eLabels { switch *l.Name { case "trace_id": if *l.Value != traceID.String() { t.Fatalf("Got %s as traceID, wanted %s", *l.Value, traceID.String()) } case "span_id": if *l.Value != spanID.String() { t.Fatalf("Got %s as spanID, wanted %s", *l.Value, spanID.String()) } default: t.Fatalf("Got unexpected label %s", *l.Name) } } } kubernetes-component-base-1b2882b/metrics/http.go000066400000000000000000000060071476422213000221030ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "io" "net/http" "time" "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( processStartedAt time.Time ) func init() { processStartedAt = time.Now() } // These constants cause handlers serving metrics to behave as described if // errors are encountered. const ( // HTTPErrorOnError serve an HTTP status code 500 upon the first error // encountered. Report the error message in the body. HTTPErrorOnError promhttp.HandlerErrorHandling = iota // ContinueOnError ignore errors and try to serve as many metrics as possible. // However, if no metrics can be served, serve an HTTP status code 500 and the // last error message in the body. Only use this in deliberate "best // effort" metrics collection scenarios. In this case, it is highly // recommended to provide other means of detecting errors: By setting an // ErrorLog in HandlerOpts, the errors are logged. By providing a // Registry in HandlerOpts, the exposed metrics include an error counter // "promhttp_metric_handler_errors_total", which can be used for // alerts. ContinueOnError // PanicOnError panics upon the first error encountered (useful for "crash only" apps). PanicOnError ) // HandlerOpts specifies options how to serve metrics via an http.Handler. The // zero value of HandlerOpts is a reasonable default. type HandlerOpts promhttp.HandlerOpts func (ho *HandlerOpts) toPromhttpHandlerOpts() promhttp.HandlerOpts { ho.ProcessStartTime = processStartedAt return promhttp.HandlerOpts(*ho) } // HandlerFor returns an uninstrumented http.Handler for the provided // Gatherer. The behavior of the Handler is defined by the provided // HandlerOpts. Thus, HandlerFor is useful to create http.Handlers for custom // Gatherers, with non-default HandlerOpts, and/or with custom (or no) // instrumentation. Use the InstrumentMetricHandler function to apply the same // kind of instrumentation as it is used by the Handler function. func HandlerFor(reg Gatherer, opts HandlerOpts) http.Handler { return promhttp.HandlerFor(reg, opts.toPromhttpHandlerOpts()) } // HandlerWithReset return an http.Handler with Reset func HandlerWithReset(reg KubeRegistry, opts HandlerOpts) http.Handler { defaultHandler := promhttp.HandlerFor(reg, opts.toPromhttpHandlerOpts()) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodDelete { reg.Reset() io.WriteString(w, "metrics reset\n") return } defaultHandler.ServeHTTP(w, r) }) } kubernetes-component-base-1b2882b/metrics/http_test.go000066400000000000000000000035301476422213000231400ustar00rootroot00000000000000/* Copyright 2020 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 metrics import ( "io" "net/http" "net/http/httptest" "testing" apimachineryversion "k8s.io/apimachinery/pkg/version" ) func TestResetHandler(t *testing.T) { currentVersion := apimachineryversion.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.1-alpha-1.12345", } registry := newKubeRegistry(currentVersion) resetHandler := HandlerWithReset(registry, HandlerOpts{}) testCases := []struct { desc string method string expectedBody string }{ { desc: "Should return empty body on a get", method: http.MethodGet, expectedBody: "", }, { desc: "Should return 'metrics reset' in the body on a delete", method: http.MethodDelete, expectedBody: "metrics reset\n", }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { req, err := http.NewRequest(tc.method, "http://sample.com/metrics", nil) if err != nil { t.Fatalf("Error creating http request") } rec := httptest.NewRecorder() resetHandler.ServeHTTP(rec, req) body, err := io.ReadAll(rec.Result().Body) if err != nil { t.Fatalf("Error reading response body") } if string(body) != tc.expectedBody { t.Errorf("Got '%s' as the response body, but want '%v'", body, tc.expectedBody) } }) } } kubernetes-component-base-1b2882b/metrics/labels.go000066400000000000000000000013461476422213000223670ustar00rootroot00000000000000/* Copyright 2019 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 metrics import "github.com/prometheus/client_golang/prometheus" // Labels represents a collection of label name -> value mappings. type Labels prometheus.Labels kubernetes-component-base-1b2882b/metrics/legacyregistry/000077500000000000000000000000001476422213000236275ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/legacyregistry/registry.go000066400000000000000000000060701476422213000260310ustar00rootroot00000000000000/* Copyright 2019 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 legacyregistry import ( "net/http" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" "k8s.io/component-base/metrics" ) var ( defaultRegistry = metrics.NewKubeRegistry() // DefaultGatherer exposes the global registry gatherer DefaultGatherer metrics.Gatherer = defaultRegistry // Reset calls reset on the global registry Reset = defaultRegistry.Reset // MustRegister registers registerable metrics but uses the global registry. MustRegister = defaultRegistry.MustRegister // RawMustRegister registers prometheus collectors but uses the global registry, this // bypasses the metric stability framework // // Deprecated RawMustRegister = defaultRegistry.RawMustRegister // Register registers a collectable metric but uses the global registry Register = defaultRegistry.Register // Registerer exposes the global registerer Registerer = defaultRegistry.Registerer processStart time.Time ) func init() { RawMustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) RawMustRegister(collectors.NewGoCollector(collectors.WithGoCollectorRuntimeMetrics(collectors.MetricsAll))) defaultRegistry.RegisterMetaMetrics() processStart = time.Now() } // Handler returns an HTTP handler for the DefaultGatherer. It is // already instrumented with InstrumentHandler (using "prometheus" as handler // name). func Handler() http.Handler { return promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, promhttp.HandlerFor(defaultRegistry, promhttp.HandlerOpts{ProcessStartTime: processStart})) } // HandlerWithReset returns an HTTP handler for the DefaultGatherer but invokes // registry reset if the http method is DELETE. func HandlerWithReset() http.Handler { return promhttp.InstrumentMetricHandler( prometheus.DefaultRegisterer, metrics.HandlerWithReset(defaultRegistry, metrics.HandlerOpts{ProcessStartTime: processStart})) } // CustomRegister registers a custom collector but uses the global registry. func CustomRegister(c metrics.StableCollector) error { err := defaultRegistry.CustomRegister(c) //TODO(RainbowMango): Maybe we can wrap this error by error wrapping.(Golang 1.13) _ = prometheus.Register(c) return err } // CustomMustRegister registers custom collectors but uses the global registry. func CustomMustRegister(cs ...metrics.StableCollector) { defaultRegistry.CustomMustRegister(cs...) for _, c := range cs { prometheus.MustRegister(c) } } kubernetes-component-base-1b2882b/metrics/legacyregistry/registry_test.go000066400000000000000000000021411476422213000270630ustar00rootroot00000000000000/* Copyright 2023 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 legacyregistry import ( "net/http" "net/http/httptest" "strconv" "testing" "time" ) const ( processStartTimeHeader = "Process-Start-Time-Unix" ) func TestProcessStartTimeHeader(t *testing.T) { now := time.Now() handler := Handler() request, _ := http.NewRequest("GET", "/", nil) writer := httptest.NewRecorder() handler.ServeHTTP(writer, request) got := writer.Header().Get(processStartTimeHeader) gotInt, _ := strconv.ParseInt(got, 10, 64) if gotInt != now.Unix() { t.Errorf("got %d, wanted %d", gotInt, now.Unix()) } } kubernetes-component-base-1b2882b/metrics/metric.go000066400000000000000000000172371476422213000224160ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "sync" "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" promext "k8s.io/component-base/metrics/prometheusextension" "k8s.io/klog/v2" ) /* kubeCollector extends the prometheus.Collector interface to allow customization of the metric registration process. Defer metric initialization until Create() is called, which then delegates to the underlying metric's initializeMetric or initializeDeprecatedMetric method call depending on whether the metric is deprecated or not. */ type kubeCollector interface { Collector lazyKubeMetric DeprecatedVersion() *semver.Version // Each collector metric should provide an initialization function // for both deprecated and non-deprecated variants of a metric. This // is necessary since metric instantiation will be deferred // until the metric is actually registered somewhere. initializeMetric() initializeDeprecatedMetric() } /* lazyKubeMetric defines our metric registration interface. lazyKubeMetric objects are expected to lazily instantiate metrics (i.e defer metric instantiation until when the Create() function is explicitly called). */ type lazyKubeMetric interface { Create(*semver.Version) bool IsCreated() bool IsHidden() bool IsDeprecated() bool } /* lazyMetric implements lazyKubeMetric. A lazy metric is lazy because it waits until metric registration time before instantiation. Add it as an anonymous field to a struct that implements kubeCollector to get deferred registration behavior. You must call lazyInit with the kubeCollector itself as an argument. */ type lazyMetric struct { fqName string isDeprecated bool isHidden bool isCreated bool createLock sync.RWMutex markDeprecationOnce sync.Once createOnce sync.Once self kubeCollector stabilityLevel StabilityLevel } func (r *lazyMetric) IsCreated() bool { r.createLock.RLock() defer r.createLock.RUnlock() return r.isCreated } // lazyInit provides the lazyMetric with a reference to the kubeCollector it is supposed // to allow lazy initialization for. It should be invoked in the factory function which creates new // kubeCollector type objects. func (r *lazyMetric) lazyInit(self kubeCollector, fqName string) { r.fqName = fqName r.self = self } // preprocessMetric figures out whether the lazy metric should be hidden or not. // This method takes a Version argument which should be the version of the binary in which // this code is currently being executed. A metric can be hidden under two conditions: // 1. if the metric is deprecated and is outside the grace period (i.e. has been // deprecated for more than one release // 2. if the metric is manually disabled via a CLI flag. // // Disclaimer: disabling a metric via a CLI flag has higher precedence than // deprecation and will override show-hidden-metrics for the explicitly // disabled metric. func (r *lazyMetric) preprocessMetric(version semver.Version) { disabledMetricsLock.RLock() defer disabledMetricsLock.RUnlock() // disabling metrics is higher in precedence than showing hidden metrics if _, ok := disabledMetrics[r.fqName]; ok { r.isHidden = true return } selfVersion := r.self.DeprecatedVersion() if selfVersion == nil { return } r.markDeprecationOnce.Do(func() { if selfVersion.LTE(version) { r.isDeprecated = true } if ShouldShowHidden() { klog.Warningf("Hidden metrics (%s) have been manually overridden, showing this very deprecated metric.", r.fqName) return } if shouldHide(&version, selfVersion) { // TODO(RainbowMango): Remove this log temporarily. https://github.com/kubernetes/kubernetes/issues/85369 // klog.Warningf("This metric has been deprecated for more than one release, hiding.") r.isHidden = true } }) } func (r *lazyMetric) IsHidden() bool { return r.isHidden } func (r *lazyMetric) IsDeprecated() bool { return r.isDeprecated } // Create forces the initialization of metric which has been deferred until // the point at which this method is invoked. This method will determine whether // the metric is deprecated or hidden, no-opting if the metric should be considered // hidden. Furthermore, this function no-opts and returns true if metric is already // created. func (r *lazyMetric) Create(version *semver.Version) bool { if version != nil { r.preprocessMetric(*version) } // let's not create if this metric is slated to be hidden if r.IsHidden() { return false } r.createOnce.Do(func() { r.createLock.Lock() defer r.createLock.Unlock() r.isCreated = true if r.IsDeprecated() { r.self.initializeDeprecatedMetric() } else { r.self.initializeMetric() } }) sl := r.stabilityLevel deprecatedV := r.self.DeprecatedVersion() dv := "" if deprecatedV != nil { dv = deprecatedV.String() } registeredMetricsTotal.WithLabelValues(string(sl), dv).Inc() return r.IsCreated() } // ClearState will clear all the states marked by Create. // It intends to be used for re-register a hidden metric. func (r *lazyMetric) ClearState() { r.createLock.Lock() defer r.createLock.Unlock() r.isDeprecated = false r.isHidden = false r.isCreated = false r.markDeprecationOnce = sync.Once{} r.createOnce = sync.Once{} } // FQName returns the fully-qualified metric name of the collector. func (r *lazyMetric) FQName() string { return r.fqName } /* This code is directly lifted from the prometheus codebase. It's a convenience struct which allows you satisfy the Collector interface automatically if you already satisfy the Metric interface. For reference: https://github.com/prometheus/client_golang/blob/v0.9.2/prometheus/collector.go#L98-L120 */ type selfCollector struct { metric prometheus.Metric } func (c *selfCollector) initSelfCollection(m prometheus.Metric) { c.metric = m } func (c *selfCollector) Describe(ch chan<- *prometheus.Desc) { ch <- c.metric.Desc() } func (c *selfCollector) Collect(ch chan<- prometheus.Metric) { ch <- c.metric } // metricWithExemplar is an interface that knows how to attach an exemplar to certain supported metric types. type metricWithExemplar interface { withExemplar(v float64) } // no-op vecs for convenience var noopCounterVec = &prometheus.CounterVec{} var noopHistogramVec = &prometheus.HistogramVec{} var noopTimingHistogramVec = &promext.TimingHistogramVec{} var noopGaugeVec = &prometheus.GaugeVec{} // just use a convenience struct for all the no-ops var noop = &noopMetric{} type noopMetric struct{} func (noopMetric) Inc() {} func (noopMetric) Add(float64) {} func (noopMetric) Dec() {} func (noopMetric) Set(float64) {} func (noopMetric) Sub(float64) {} func (noopMetric) Observe(float64) {} func (noopMetric) ObserveWithWeight(float64, uint64) {} func (noopMetric) SetToCurrentTime() {} func (noopMetric) Desc() *prometheus.Desc { return nil } func (noopMetric) Write(*dto.Metric) error { return nil } func (noopMetric) Describe(chan<- *prometheus.Desc) {} func (noopMetric) Collect(chan<- prometheus.Metric) {} kubernetes-component-base-1b2882b/metrics/options.go000066400000000000000000000110071476422213000226130ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "fmt" "regexp" "github.com/blang/semver/v4" "github.com/spf13/pflag" "k8s.io/component-base/version" ) // Options has all parameters needed for exposing metrics from components type Options struct { ShowHiddenMetricsForVersion string DisabledMetrics []string AllowListMapping map[string]string AllowListMappingManifest string } // NewOptions returns default metrics options func NewOptions() *Options { return &Options{} } // Validate validates metrics flags options. func (o *Options) Validate() []error { if o == nil { return nil } var errs []error err := validateShowHiddenMetricsVersion(parseVersion(version.Get()), o.ShowHiddenMetricsForVersion) if err != nil { errs = append(errs, err) } if err := validateAllowMetricLabel(o.AllowListMapping); err != nil { errs = append(errs, err) } if len(errs) == 0 { return nil } return errs } // AddFlags adds flags for exposing component metrics. func (o *Options) AddFlags(fs *pflag.FlagSet) { if o == nil { return } fs.StringVar(&o.ShowHiddenMetricsForVersion, "show-hidden-metrics-for-version", o.ShowHiddenMetricsForVersion, "The previous version for which you want to show hidden metrics. "+ "Only the previous minor version is meaningful, other values will not be allowed. "+ "The format is ., e.g.: '1.16'. "+ "The purpose of this format is make sure you have the opportunity to notice if the next release hides additional metrics, "+ "rather than being surprised when they are permanently removed in the release after that.") fs.StringSliceVar(&o.DisabledMetrics, "disabled-metrics", o.DisabledMetrics, "This flag provides an escape hatch for misbehaving metrics. "+ "You must provide the fully qualified metric name in order to disable it. "+ "Disclaimer: disabling metrics is higher in precedence than showing hidden metrics.") fs.StringToStringVar(&o.AllowListMapping, "allow-metric-labels", o.AllowListMapping, "The map from metric-label to value allow-list of this label. The key's format is ,. "+ "The value's format is ,..."+ "e.g. metric1,label1='v1,v2,v3', metric1,label2='v1,v2,v3' metric2,label1='v1,v2,v3'.") fs.StringVar(&o.AllowListMappingManifest, "allow-metric-labels-manifest", o.AllowListMappingManifest, "The path to the manifest file that contains the allow-list mapping. "+ "The format of the file is the same as the flag --allow-metric-labels. "+ "Note that the flag --allow-metric-labels will override the manifest file.") } // Apply applies parameters into global configuration of metrics. func (o *Options) Apply() { if o == nil { return } if len(o.ShowHiddenMetricsForVersion) > 0 { SetShowHidden() } // set disabled metrics for _, metricName := range o.DisabledMetrics { SetDisabledMetric(metricName) } if o.AllowListMapping != nil { SetLabelAllowListFromCLI(o.AllowListMapping) } else if len(o.AllowListMappingManifest) > 0 { SetLabelAllowListFromManifest(o.AllowListMappingManifest) } } func validateShowHiddenMetricsVersion(currentVersion semver.Version, targetVersionStr string) error { if targetVersionStr == "" { return nil } validVersionStr := fmt.Sprintf("%d.%d", currentVersion.Major, currentVersion.Minor-1) if targetVersionStr != validVersionStr { return fmt.Errorf("--show-hidden-metrics-for-version must be omitted or have the value '%v'. Only the previous minor version is allowed", validVersionStr) } return nil } func validateAllowMetricLabel(allowListMapping map[string]string) error { if allowListMapping == nil { return nil } metricNameRegex := `[a-zA-Z_:][a-zA-Z0-9_:]*` labelRegex := `[a-zA-Z_][a-zA-Z0-9_]*` for k := range allowListMapping { reg := regexp.MustCompile(metricNameRegex + `,` + labelRegex) if reg.FindString(k) != k { return fmt.Errorf("--allow-metric-labels must have a list of kv pair with format `metricName,labelName=labelValue, labelValue,...`") } } return nil } kubernetes-component-base-1b2882b/metrics/options_test.go000066400000000000000000000030631476422213000236550ustar00rootroot00000000000000/* Copyright 2021 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 metrics import "testing" func TestValidateAllowMetricLabel(t *testing.T) { var tests = []struct { name string input map[string]string expectedError bool }{ { "validated", map[string]string{ "metric_name,label_name": "labelValue1,labelValue2", }, false, }, { "metric name is not valid", map[string]string{ "-metric_name,label_name": "labelValue1,labelValue2", }, true, }, { "label name is not valid", map[string]string{ "metric_name,:label_name": "labelValue1,labelValue2", }, true, }, { "no label name", map[string]string{ "metric_name": "labelValue1,labelValue2", }, true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateAllowMetricLabel(tt.input) if err == nil && tt.expectedError { t.Error("Got error is nil, wanted error is not nil") } if err != nil && !tt.expectedError { t.Errorf("Got error is %v, wanted no error", err) } }) } } kubernetes-component-base-1b2882b/metrics/opts.go000066400000000000000000000322231476422213000221100ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "fmt" "os" "path/filepath" "strings" "sync" "time" "github.com/prometheus/client_golang/prometheus" "k8s.io/apimachinery/pkg/util/sets" promext "k8s.io/component-base/metrics/prometheusextension" "k8s.io/klog/v2" yaml "sigs.k8s.io/yaml/goyaml.v2" ) var ( labelValueAllowLists = map[string]*MetricLabelAllowList{} allowListLock sync.RWMutex ) // ResetLabelValueAllowLists resets the allow lists for label values. // NOTE: This should only be used in test. func ResetLabelValueAllowLists() { allowListLock.Lock() defer allowListLock.Unlock() labelValueAllowLists = map[string]*MetricLabelAllowList{} } // KubeOpts is superset struct for prometheus.Opts. The prometheus Opts structure // is purposefully not embedded here because that would change struct initialization // in the manner which people are currently accustomed. // // Name must be set to a non-empty string. DeprecatedVersion is defined only // if the metric for which this options applies is, in fact, deprecated. type KubeOpts struct { Namespace string Subsystem string Name string Help string ConstLabels map[string]string DeprecatedVersion string deprecateOnce sync.Once annotateOnce sync.Once StabilityLevel StabilityLevel initializeLabelAllowListsOnce sync.Once LabelValueAllowLists *MetricLabelAllowList } // BuildFQName joins the given three name components by "_". Empty name // components are ignored. If the name parameter itself is empty, an empty // string is returned, no matter what. Metric implementations included in this // library use this function internally to generate the fully-qualified metric // name from the name component in their Opts. Users of the library will only // need this function if they implement their own Metric or instantiate a Desc // (with NewDesc) directly. func BuildFQName(namespace, subsystem, name string) string { return prometheus.BuildFQName(namespace, subsystem, name) } // StabilityLevel represents the API guarantees for a given defined metric. type StabilityLevel string const ( // INTERNAL metrics have no stability guarantees, as such, labels may // be arbitrarily added/removed and the metric may be deleted at any time. INTERNAL StabilityLevel = "INTERNAL" // ALPHA metrics have no stability guarantees, as such, labels may // be arbitrarily added/removed and the metric may be deleted at any time. ALPHA StabilityLevel = "ALPHA" // BETA metrics are governed by the deprecation policy outlined in by // the control plane metrics stability KEP. BETA StabilityLevel = "BETA" // STABLE metrics are guaranteed not be mutated and removal is governed by // the deprecation policy outlined in by the control plane metrics stability KEP. STABLE StabilityLevel = "STABLE" ) // setDefaults takes 'ALPHA' in case of empty. func (sl *StabilityLevel) setDefaults() { switch *sl { case "": *sl = ALPHA default: // no-op, since we have a StabilityLevel already } } // CounterOpts is an alias for Opts. See there for doc comments. type CounterOpts KubeOpts // Modify help description on the metric description. func (o *CounterOpts) markDeprecated() { o.deprecateOnce.Do(func() { o.Help = fmt.Sprintf("(Deprecated since %v) %v", o.DeprecatedVersion, o.Help) }) } // annotateStabilityLevel annotates help description on the metric description with the stability level // of the metric func (o *CounterOpts) annotateStabilityLevel() { o.annotateOnce.Do(func() { o.Help = fmt.Sprintf("[%v] %v", o.StabilityLevel, o.Help) }) } // convenience function to allow easy transformation to the prometheus // counterpart. This will do more once we have a proper label abstraction func (o *CounterOpts) toPromCounterOpts() prometheus.CounterOpts { return prometheus.CounterOpts{ Namespace: o.Namespace, Subsystem: o.Subsystem, Name: o.Name, Help: o.Help, ConstLabels: o.ConstLabels, } } // GaugeOpts is an alias for Opts. See there for doc comments. type GaugeOpts KubeOpts // Modify help description on the metric description. func (o *GaugeOpts) markDeprecated() { o.deprecateOnce.Do(func() { o.Help = fmt.Sprintf("(Deprecated since %v) %v", o.DeprecatedVersion, o.Help) }) } // annotateStabilityLevel annotates help description on the metric description with the stability level // of the metric func (o *GaugeOpts) annotateStabilityLevel() { o.annotateOnce.Do(func() { o.Help = fmt.Sprintf("[%v] %v", o.StabilityLevel, o.Help) }) } // convenience function to allow easy transformation to the prometheus // counterpart. This will do more once we have a proper label abstraction func (o *GaugeOpts) toPromGaugeOpts() prometheus.GaugeOpts { return prometheus.GaugeOpts{ Namespace: o.Namespace, Subsystem: o.Subsystem, Name: o.Name, Help: o.Help, ConstLabels: o.ConstLabels, } } // HistogramOpts bundles the options for creating a Histogram metric. It is // mandatory to set Name to a non-empty string. All other fields are optional // and can safely be left at their zero value, although it is strongly // encouraged to set a Help string. type HistogramOpts struct { Namespace string Subsystem string Name string Help string ConstLabels map[string]string Buckets []float64 DeprecatedVersion string deprecateOnce sync.Once annotateOnce sync.Once StabilityLevel StabilityLevel initializeLabelAllowListsOnce sync.Once LabelValueAllowLists *MetricLabelAllowList } // Modify help description on the metric description. func (o *HistogramOpts) markDeprecated() { o.deprecateOnce.Do(func() { o.Help = fmt.Sprintf("(Deprecated since %v) %v", o.DeprecatedVersion, o.Help) }) } // annotateStabilityLevel annotates help description on the metric description with the stability level // of the metric func (o *HistogramOpts) annotateStabilityLevel() { o.annotateOnce.Do(func() { o.Help = fmt.Sprintf("[%v] %v", o.StabilityLevel, o.Help) }) } // convenience function to allow easy transformation to the prometheus // counterpart. This will do more once we have a proper label abstraction func (o *HistogramOpts) toPromHistogramOpts() prometheus.HistogramOpts { return prometheus.HistogramOpts{ Namespace: o.Namespace, Subsystem: o.Subsystem, Name: o.Name, Help: o.Help, ConstLabels: o.ConstLabels, Buckets: o.Buckets, } } // TimingHistogramOpts bundles the options for creating a TimingHistogram metric. It is // mandatory to set Name to a non-empty string. All other fields are optional // and can safely be left at their zero value, although it is strongly // encouraged to set a Help string. type TimingHistogramOpts struct { Namespace string Subsystem string Name string Help string ConstLabels map[string]string Buckets []float64 InitialValue float64 DeprecatedVersion string deprecateOnce sync.Once annotateOnce sync.Once StabilityLevel StabilityLevel initializeLabelAllowListsOnce sync.Once LabelValueAllowLists *MetricLabelAllowList } // Modify help description on the metric description. func (o *TimingHistogramOpts) markDeprecated() { o.deprecateOnce.Do(func() { o.Help = fmt.Sprintf("(Deprecated since %v) %v", o.DeprecatedVersion, o.Help) }) } // annotateStabilityLevel annotates help description on the metric description with the stability level // of the metric func (o *TimingHistogramOpts) annotateStabilityLevel() { o.annotateOnce.Do(func() { o.Help = fmt.Sprintf("[%v] %v", o.StabilityLevel, o.Help) }) } // convenience function to allow easy transformation to the prometheus // counterpart. This will do more once we have a proper label abstraction func (o *TimingHistogramOpts) toPromHistogramOpts() promext.TimingHistogramOpts { return promext.TimingHistogramOpts{ Namespace: o.Namespace, Subsystem: o.Subsystem, Name: o.Name, Help: o.Help, ConstLabels: o.ConstLabels, Buckets: o.Buckets, InitialValue: o.InitialValue, } } // SummaryOpts bundles the options for creating a Summary metric. It is // mandatory to set Name to a non-empty string. While all other fields are // optional and can safely be left at their zero value, it is recommended to set // a help string and to explicitly set the Objectives field to the desired value // as the default value will change in the upcoming v0.10 of the library. type SummaryOpts struct { Namespace string Subsystem string Name string Help string ConstLabels map[string]string Objectives map[float64]float64 MaxAge time.Duration AgeBuckets uint32 BufCap uint32 DeprecatedVersion string deprecateOnce sync.Once annotateOnce sync.Once StabilityLevel StabilityLevel initializeLabelAllowListsOnce sync.Once LabelValueAllowLists *MetricLabelAllowList } // Modify help description on the metric description. func (o *SummaryOpts) markDeprecated() { o.deprecateOnce.Do(func() { o.Help = fmt.Sprintf("(Deprecated since %v) %v", o.DeprecatedVersion, o.Help) }) } // annotateStabilityLevel annotates help description on the metric description with the stability level // of the metric func (o *SummaryOpts) annotateStabilityLevel() { o.annotateOnce.Do(func() { o.Help = fmt.Sprintf("[%v] %v", o.StabilityLevel, o.Help) }) } // Deprecated: DefObjectives will not be used as the default objectives in // v1.0.0 of the library. The default Summary will have no quantiles then. var ( defObjectives = map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001} ) // convenience function to allow easy transformation to the prometheus // counterpart. This will do more once we have a proper label abstraction func (o *SummaryOpts) toPromSummaryOpts() prometheus.SummaryOpts { // we need to retain existing quantile behavior for backwards compatibility, // so let's do what prometheus used to do prior to v1. objectives := o.Objectives if objectives == nil { objectives = defObjectives } return prometheus.SummaryOpts{ Namespace: o.Namespace, Subsystem: o.Subsystem, Name: o.Name, Help: o.Help, ConstLabels: o.ConstLabels, Objectives: objectives, MaxAge: o.MaxAge, AgeBuckets: o.AgeBuckets, BufCap: o.BufCap, } } type MetricLabelAllowList struct { labelToAllowList map[string]sets.Set[string] } func (allowList *MetricLabelAllowList) ConstrainToAllowedList(labelNameList, labelValueList []string) { for index, value := range labelValueList { name := labelNameList[index] if allowValues, ok := allowList.labelToAllowList[name]; ok { if !allowValues.Has(value) { labelValueList[index] = "unexpected" cardinalityEnforcementUnexpectedCategorizationsTotal.Inc() } } } } func (allowList *MetricLabelAllowList) ConstrainLabelMap(labels map[string]string) { for name, value := range labels { if allowValues, ok := allowList.labelToAllowList[name]; ok { if !allowValues.Has(value) { labels[name] = "unexpected" cardinalityEnforcementUnexpectedCategorizationsTotal.Inc() } } } } func SetLabelAllowListFromCLI(allowListMapping map[string]string) { allowListLock.Lock() defer allowListLock.Unlock() for metricLabelName, labelValues := range allowListMapping { metricName := strings.Split(metricLabelName, ",")[0] labelName := strings.Split(metricLabelName, ",")[1] valueSet := sets.New[string](strings.Split(labelValues, ",")...) allowList, ok := labelValueAllowLists[metricName] if ok { allowList.labelToAllowList[labelName] = valueSet } else { labelToAllowList := make(map[string]sets.Set[string]) labelToAllowList[labelName] = valueSet labelValueAllowLists[metricName] = &MetricLabelAllowList{ labelToAllowList, } } } } func SetLabelAllowListFromManifest(manifest string) { allowListMapping := make(map[string]string) data, err := os.ReadFile(filepath.Clean(manifest)) if err != nil { klog.Errorf("Failed to read allow list manifest: %v", err) return } err = yaml.Unmarshal(data, &allowListMapping) if err != nil { klog.Errorf("Failed to parse allow list manifest: %v", err) return } SetLabelAllowListFromCLI(allowListMapping) } kubernetes-component-base-1b2882b/metrics/opts_test.go000066400000000000000000000134761476422213000231600ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "os" "reflect" "testing" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/util/sets" ) func TestDefaultStabilityLevel(t *testing.T) { var tests = []struct { name string inputValue StabilityLevel expectValue StabilityLevel expectPanic bool }{ { name: "empty should take ALPHA by default", inputValue: "", expectValue: ALPHA, expectPanic: false, }, { name: "INTERNAL remain unchanged", inputValue: INTERNAL, expectValue: INTERNAL, expectPanic: false, }, { name: "STABLE remain unchanged", inputValue: STABLE, expectValue: STABLE, expectPanic: false, }, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { var stability = tc.inputValue stability.setDefaults() assert.Equalf(t, tc.expectValue, stability, "Got %s, expected: %v ", stability, tc.expectValue) }) } } func TestConstrainToAllowedList(t *testing.T) { allowList := &MetricLabelAllowList{ labelToAllowList: map[string]sets.Set[string]{ "label_a": sets.New[string]("allow_value1", "allow_value2"), }, } labelNameList := []string{"label_a", "label_b"} var tests = []struct { name string inputLabelValueList []string outputLabelValueList []string }{ { "no unexpected value", []string{"allow_value1", "label_b_value"}, []string{"allow_value1", "label_b_value"}, }, { "with unexpected value", []string{"not_allowed", "label_b_value"}, []string{"unexpected", "label_b_value"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { allowList.ConstrainToAllowedList(labelNameList, test.inputLabelValueList) if !reflect.DeepEqual(test.inputLabelValueList, test.outputLabelValueList) { t.Errorf("Got %v, expected %v", test.inputLabelValueList, test.outputLabelValueList) } }) } } func TestConstrainLabelMap(t *testing.T) { allowList := &MetricLabelAllowList{ labelToAllowList: map[string]sets.Set[string]{ "label_a": sets.New[string]("allow_value1", "allow_value2"), }, } var tests = []struct { name string inputLabelMap map[string]string outputLabelMap map[string]string }{ { "no unexpected value", map[string]string{ "label_a": "allow_value1", "label_b": "label_b_value", }, map[string]string{ "label_a": "allow_value1", "label_b": "label_b_value", }, }, { "with unexpected value", map[string]string{ "label_a": "not_allowed", "label_b": "label_b_value", }, map[string]string{ "label_a": "unexpected", "label_b": "label_b_value", }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { allowList.ConstrainLabelMap(test.inputLabelMap) if !reflect.DeepEqual(test.inputLabelMap, test.outputLabelMap) { t.Errorf("Got %v, expected %v", test.inputLabelMap, test.outputLabelMap) } }) } } func TestSetLabelAllowListFromManifest(t *testing.T) { tests := []struct { name string manifest string manifestExist bool expectlabelValueAllowLists map[string]*MetricLabelAllowList }{ { name: "successfully parse manifest", manifestExist: true, manifest: `metric1,label1: v1,v2 metric2,label2: v3`, expectlabelValueAllowLists: map[string]*MetricLabelAllowList{ "metric1": { labelToAllowList: map[string]sets.Set[string]{ "label1": sets.New[string]("v1", "v2"), }, }, "metric2": { labelToAllowList: map[string]sets.Set[string]{ "label2": sets.New[string]("v3"), }, }, }, }, { name: "failed to read manifest file", manifestExist: false, expectlabelValueAllowLists: map[string]*MetricLabelAllowList{}, }, { name: "failed to parse manifest", manifestExist: true, manifest: `allow-list: - metric1,label1:v1 - metric2,label2:v2,v3`, expectlabelValueAllowLists: map[string]*MetricLabelAllowList{}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { labelValueAllowLists = map[string]*MetricLabelAllowList{} manifestFilePath := "/non-existent-file.yaml" if tc.manifestExist { tempFile, err := os.CreateTemp("", "allow-list-test") if err != nil { t.Fatalf("failed to create temp file: %v", err) } defer func() { if err := os.Remove(tempFile.Name()); err != nil { t.Errorf("failed to remove temp file: %v", err) } }() if _, err := tempFile.WriteString(tc.manifest); err != nil { t.Fatalf("failed to write to temp file: %v", err) } manifestFilePath = tempFile.Name() } SetLabelAllowListFromManifest(manifestFilePath) if !reflect.DeepEqual(labelValueAllowLists, tc.expectlabelValueAllowLists) { t.Errorf("labelValueAllowLists = %+v, want %+v", labelValueAllowLists, tc.expectlabelValueAllowLists) } }) } } func TestResetLabelValueAllowLists(t *testing.T) { labelValueAllowLists = map[string]*MetricLabelAllowList{ "metric1": { labelToAllowList: map[string]sets.Set[string]{ "label1": sets.New[string]("v1", "v2"), }, }, "metric2": { labelToAllowList: map[string]sets.Set[string]{ "label2": sets.New[string]("v3"), }, }, } ResetLabelValueAllowLists() assert.Empty(t, labelValueAllowLists) } kubernetes-component-base-1b2882b/metrics/processstarttime.go000066400000000000000000000030001476422213000245250ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "time" "k8s.io/klog/v2" ) var processStartTime = NewGaugeVec( &GaugeOpts{ Name: "process_start_time_seconds", Help: "Start time of the process since unix epoch in seconds.", StabilityLevel: ALPHA, }, []string{}, ) // RegisterProcessStartTime registers the process_start_time_seconds to // a prometheus registry. This metric needs to be included to ensure counter // data fidelity. func RegisterProcessStartTime(registrationFunc func(Registerable) error) error { start, err := GetProcessStart() if err != nil { klog.Errorf("Could not get process start time, %v", err) start = float64(time.Now().Unix()) } // processStartTime is a lazy metric which only get initialized after registered. // so we need to register the metric first and then set the value for it if err = registrationFunc(processStartTime); err != nil { return err } processStartTime.WithLabelValues().Set(start) return nil } kubernetes-component-base-1b2882b/metrics/processstarttime_others.go000066400000000000000000000015771476422213000261320ustar00rootroot00000000000000//go:build !windows // +build !windows /* Copyright 2019 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 metrics import ( "os" "github.com/prometheus/procfs" ) func GetProcessStart() (float64, error) { pid := os.Getpid() p, err := procfs.NewProc(pid) if err != nil { return 0, err } if stat, err := p.Stat(); err == nil { return stat.StartTime() } return 0, err } kubernetes-component-base-1b2882b/metrics/processstarttime_windows.go000066400000000000000000000017611476422213000263130ustar00rootroot00000000000000//go:build windows // +build windows /* Copyright 2019 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 metrics import ( "golang.org/x/sys/windows" ) func GetProcessStart() (float64, error) { processHandle := windows.CurrentProcess() var creationTime, exitTime, kernelTime, userTime windows.Filetime if err := windows.GetProcessTimes(processHandle, &creationTime, &exitTime, &kernelTime, &userTime); err != nil { return 0, err } return float64(creationTime.Nanoseconds() / 1e9), nil } kubernetes-component-base-1b2882b/metrics/prometheus/000077500000000000000000000000001476422213000227655ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/prometheus/clientgo/000077500000000000000000000000001476422213000245715ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/prometheus/clientgo/leaderelection/000077500000000000000000000000001476422213000275505ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/prometheus/clientgo/leaderelection/metrics.go000066400000000000000000000046131476422213000315510ustar00rootroot00000000000000/* Copyright 2018 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 leaderelection import ( "k8s.io/client-go/tools/leaderelection" k8smetrics "k8s.io/component-base/metrics" "k8s.io/component-base/metrics/legacyregistry" ) var ( leaderGauge = k8smetrics.NewGaugeVec(&k8smetrics.GaugeOpts{ Name: "leader_election_master_status", StabilityLevel: k8smetrics.ALPHA, Help: "Gauge of if the reporting system is master of the relevant lease, 0 indicates backup, 1 indicates master. 'name' is the string used to identify the lease. Please make sure to group by name.", }, []string{"name"}) // A cumulative counter should be sufficient to get a rough ratio of slow path // exercised given the leader election frequency is specified explicitly. So that // to avoid the overhead to report a counter exercising fastpath. leaderSlowpathCounter = k8smetrics.NewCounterVec(&k8smetrics.CounterOpts{ Name: "leader_election_slowpath_total", StabilityLevel: k8smetrics.ALPHA, Help: "Total number of slow path exercised in renewing leader leases. 'name' is the string used to identify the lease. Please make sure to group by name.", }, []string{"name"}) ) func init() { legacyregistry.MustRegister(leaderGauge) legacyregistry.MustRegister(leaderSlowpathCounter) leaderelection.SetProvider(prometheusMetricsProvider{}) } type prometheusMetricsProvider struct{} func (prometheusMetricsProvider) NewLeaderMetric() leaderelection.LeaderMetric { return &leaderAdapter{gauge: leaderGauge, counter: leaderSlowpathCounter} } type leaderAdapter struct { gauge *k8smetrics.GaugeVec counter *k8smetrics.CounterVec } func (s *leaderAdapter) On(name string) { s.gauge.WithLabelValues(name).Set(1.0) } func (s *leaderAdapter) Off(name string) { s.gauge.WithLabelValues(name).Set(0.0) } func (s *leaderAdapter) SlowpathExercised(name string) { s.counter.WithLabelValues(name).Inc() } kubernetes-component-base-1b2882b/metrics/prometheus/clientgo/metrics.go000066400000000000000000000016021476422213000265650ustar00rootroot00000000000000/* Copyright 2019 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 clientgo import ( _ "k8s.io/component-base/metrics/prometheus/clientgo/leaderelection" // load leaderelection metrics _ "k8s.io/component-base/metrics/prometheus/restclient" // load restclient metrics _ "k8s.io/component-base/metrics/prometheus/workqueue" // load the workqueue metrics ) kubernetes-component-base-1b2882b/metrics/prometheus/controllers/000077500000000000000000000000001476422213000253335ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/prometheus/controllers/metrics.go000066400000000000000000000042421476422213000273320ustar00rootroot00000000000000/* Copyright 2021 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 controllers import ( "sync" k8smetrics "k8s.io/component-base/metrics" "k8s.io/component-base/metrics/legacyregistry" ) var ( once sync.Once controllerInstanceCount = k8smetrics.NewGaugeVec( &k8smetrics.GaugeOpts{ Name: "running_managed_controllers", Help: "Indicates where instances of a controller are currently running", StabilityLevel: k8smetrics.ALPHA, }, []string{"name", "manager"}, ) ) // ControllerManagerMetrics is a proxy to set controller manager specific metrics. type ControllerManagerMetrics struct { manager string } // NewControllerManagerMetrics create a new ControllerManagerMetrics, with specific manager name. func NewControllerManagerMetrics(manager string) *ControllerManagerMetrics { controllerMetrics := &ControllerManagerMetrics{ manager: manager, } return controllerMetrics } // Register controller manager metrics. func Register() { once.Do(func() { legacyregistry.MustRegister(controllerInstanceCount) }) } // ControllerStarted sets the controllerInstanceCount to 1. // These values use set instead of inc/dec to avoid accidentally double counting // a controller that starts but fails to properly signal when it crashes. func (a *ControllerManagerMetrics) ControllerStarted(name string) { controllerInstanceCount.With(k8smetrics.Labels{"name": name, "manager": a.manager}).Set(float64(1)) } // ControllerStopped sets the controllerInstanceCount to 0. func (a *ControllerManagerMetrics) ControllerStopped(name string) { controllerInstanceCount.With(k8smetrics.Labels{"name": name, "manager": a.manager}).Set(float64(0)) } kubernetes-component-base-1b2882b/metrics/prometheus/feature/000077500000000000000000000000001476422213000244205ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/prometheus/feature/metrics.go000066400000000000000000000026361476422213000264240ustar00rootroot00000000000000/* Copyright 2022 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 feature import ( "context" k8smetrics "k8s.io/component-base/metrics" "k8s.io/component-base/metrics/legacyregistry" ) var ( // featureInfo is a Prometheus Gauge metrics used for recording the enablement of a k8s feature. featureInfo = k8smetrics.NewGaugeVec( &k8smetrics.GaugeOpts{ Namespace: "kubernetes", Name: "feature_enabled", Help: "This metric records the data about the stage and enablement of a k8s feature.", StabilityLevel: k8smetrics.BETA, }, []string{"name", "stage"}, ) ) func init() { legacyregistry.MustRegister(featureInfo) } func ResetFeatureInfoMetric() { featureInfo.Reset() } func RecordFeatureInfo(ctx context.Context, name string, stage string, enabled bool) { value := 0.0 if enabled { value = 1.0 } featureInfo.WithContext(ctx).WithLabelValues(name, stage).Set(value) } kubernetes-component-base-1b2882b/metrics/prometheus/feature/metrics_test.go000066400000000000000000000040441476422213000274560ustar00rootroot00000000000000/* Copyright 2022 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 feature import ( "context" "strings" "testing" "k8s.io/component-base/metrics/legacyregistry" "k8s.io/component-base/metrics/testutil" ) var ( testedMetrics = []string{"kubernetes_feature_enabled"} ) func TestObserveHealthcheck(t *testing.T) { defer legacyregistry.Reset() defer ResetFeatureInfoMetric() testCases := []struct { desc string name string stage string enabled bool want string }{ { desc: "test enabled", name: "feature-a", stage: "ALPHA", enabled: true, want: ` # HELP kubernetes_feature_enabled [BETA] This metric records the data about the stage and enablement of a k8s feature. # TYPE kubernetes_feature_enabled gauge kubernetes_feature_enabled{name="feature-a",stage="ALPHA"} 1 `, }, { desc: "test disabled", name: "feature-b", stage: "BETA", enabled: false, want: ` # HELP kubernetes_feature_enabled [BETA] This metric records the data about the stage and enablement of a k8s feature. # TYPE kubernetes_feature_enabled gauge kubernetes_feature_enabled{name="feature-b",stage="BETA"} 0 `, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer ResetFeatureInfoMetric() RecordFeatureInfo(context.Background(), test.name, test.stage, test.enabled) if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(test.want), testedMetrics...); err != nil { t.Fatal(err) } }) } } kubernetes-component-base-1b2882b/metrics/prometheus/meta/000077500000000000000000000000001476422213000237135ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/prometheus/meta/metrics.go000066400000000000000000000025701476422213000257140ustar00rootroot00000000000000/* Copyright 2023 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 meta import ( k8smetrics "k8s.io/component-base/metrics" ) var ( RegisteredMetrics = k8smetrics.NewCounterVec( &k8smetrics.CounterOpts{ Name: "registered_metrics_total", Help: "The count of registered metrics broken by stability level and deprecation version.", StabilityLevel: k8smetrics.BETA, }, []string{"stability_level", "deprecated_version"}, ) DisabledMetricsTotal = k8smetrics.NewCounter( &k8smetrics.CounterOpts{ Name: "disabled_metrics_total", Help: "The count of disabled metrics.", StabilityLevel: k8smetrics.BETA, }, ) HiddenMetricsTotal = k8smetrics.NewCounter( &k8smetrics.CounterOpts{ Name: "hidden_metrics_total", Help: "The count of hidden metrics.", StabilityLevel: k8smetrics.BETA, }, ) ) kubernetes-component-base-1b2882b/metrics/prometheus/restclient/000077500000000000000000000000001476422213000251415ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/prometheus/restclient/metrics.go000066400000000000000000000231451476422213000271430ustar00rootroot00000000000000/* Copyright 2016 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 restclient import ( "context" "fmt" "math" "net/url" "time" "k8s.io/client-go/tools/metrics" k8smetrics "k8s.io/component-base/metrics" "k8s.io/component-base/metrics/legacyregistry" ) var ( // requestLatency is a Prometheus Histogram metric type partitioned by // "verb", and "host" labels. It is used for the rest client latency metrics. requestLatency = k8smetrics.NewHistogramVec( &k8smetrics.HistogramOpts{ Name: "rest_client_request_duration_seconds", Help: "Request latency in seconds. Broken down by verb, and host.", StabilityLevel: k8smetrics.ALPHA, Buckets: []float64{0.005, 0.025, 0.1, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 15.0, 30.0, 60.0}, }, []string{"verb", "host"}, ) // resolverLatency is a Prometheus Histogram metric type partitioned by // "host" labels. It is used for the rest client DNS resolver latency metrics. resolverLatency = k8smetrics.NewHistogramVec( &k8smetrics.HistogramOpts{ Name: "rest_client_dns_resolution_duration_seconds", Help: "DNS resolver latency in seconds. Broken down by host.", StabilityLevel: k8smetrics.ALPHA, Buckets: []float64{0.005, 0.025, 0.1, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 15.0, 30.0}, }, []string{"host"}, ) requestSize = k8smetrics.NewHistogramVec( &k8smetrics.HistogramOpts{ Name: "rest_client_request_size_bytes", Help: "Request size in bytes. Broken down by verb and host.", StabilityLevel: k8smetrics.ALPHA, // 64 bytes to 16MB Buckets: []float64{64, 256, 512, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216}, }, []string{"verb", "host"}, ) responseSize = k8smetrics.NewHistogramVec( &k8smetrics.HistogramOpts{ Name: "rest_client_response_size_bytes", Help: "Response size in bytes. Broken down by verb and host.", StabilityLevel: k8smetrics.ALPHA, // 64 bytes to 16MB Buckets: []float64{64, 256, 512, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216}, }, []string{"verb", "host"}, ) rateLimiterLatency = k8smetrics.NewHistogramVec( &k8smetrics.HistogramOpts{ Name: "rest_client_rate_limiter_duration_seconds", Help: "Client side rate limiter latency in seconds. Broken down by verb, and host.", StabilityLevel: k8smetrics.ALPHA, Buckets: []float64{0.005, 0.025, 0.1, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 15.0, 30.0, 60.0}, }, []string{"verb", "host"}, ) requestResult = k8smetrics.NewCounterVec( &k8smetrics.CounterOpts{ Name: "rest_client_requests_total", StabilityLevel: k8smetrics.ALPHA, Help: "Number of HTTP requests, partitioned by status code, method, and host.", }, []string{"code", "method", "host"}, ) requestRetry = k8smetrics.NewCounterVec( &k8smetrics.CounterOpts{ Name: "rest_client_request_retries_total", StabilityLevel: k8smetrics.ALPHA, Help: "Number of request retries, partitioned by status code, verb, and host.", }, []string{"code", "verb", "host"}, ) execPluginCertTTLAdapter = &expiryToTTLAdapter{} execPluginCertTTL = k8smetrics.NewGaugeFunc( &k8smetrics.GaugeOpts{ Name: "rest_client_exec_plugin_ttl_seconds", Help: "Gauge of the shortest TTL (time-to-live) of the client " + "certificate(s) managed by the auth exec plugin. The value " + "is in seconds until certificate expiry (negative if " + "already expired). If auth exec plugins are unused or manage no " + "TLS certificates, the value will be +INF.", StabilityLevel: k8smetrics.ALPHA, }, func() float64 { if execPluginCertTTLAdapter.e == nil { return math.Inf(1) } return execPluginCertTTLAdapter.e.Sub(time.Now()).Seconds() }, ) execPluginCertRotation = k8smetrics.NewHistogram( &k8smetrics.HistogramOpts{ Name: "rest_client_exec_plugin_certificate_rotation_age", Help: "Histogram of the number of seconds the last auth exec " + "plugin client certificate lived before being rotated. " + "If auth exec plugin client certificates are unused, " + "histogram will contain no data.", // There are three sets of ranges these buckets intend to capture: // - 10-60 minutes: captures a rotation cadence which is // happening too quickly. // - 4 hours - 1 month: captures an ideal rotation cadence. // - 3 months - 4 years: captures a rotation cadence which is // is probably too slow or much too slow. StabilityLevel: k8smetrics.ALPHA, Buckets: []float64{ 600, // 10 minutes 1800, // 30 minutes 3600, // 1 hour 14400, // 4 hours 86400, // 1 day 604800, // 1 week 2592000, // 1 month 7776000, // 3 months 15552000, // 6 months 31104000, // 1 year 124416000, // 4 years }, }, ) execPluginCalls = k8smetrics.NewCounterVec( &k8smetrics.CounterOpts{ StabilityLevel: k8smetrics.ALPHA, Name: "rest_client_exec_plugin_call_total", Help: "Number of calls to an exec plugin, partitioned by the type of " + "event encountered (no_error, plugin_execution_error, plugin_not_found_error, " + "client_internal_error) and an optional exit code. The exit code will " + "be set to 0 if and only if the plugin call was successful.", }, []string{"code", "call_status"}, ) transportCacheEntries = k8smetrics.NewGauge( &k8smetrics.GaugeOpts{ Name: "rest_client_transport_cache_entries", StabilityLevel: k8smetrics.ALPHA, Help: "Number of transport entries in the internal cache.", }, ) transportCacheCalls = k8smetrics.NewCounterVec( &k8smetrics.CounterOpts{ Name: "rest_client_transport_create_calls_total", StabilityLevel: k8smetrics.ALPHA, Help: "Number of calls to get a new transport, partitioned by the result of the operation " + "hit: obtained from the cache, miss: created and added to the cache, uncacheable: created and not cached", }, []string{"result"}, ) ) func init() { legacyregistry.MustRegister(requestLatency) legacyregistry.MustRegister(requestSize) legacyregistry.MustRegister(responseSize) legacyregistry.MustRegister(rateLimiterLatency) legacyregistry.MustRegister(requestResult) legacyregistry.MustRegister(requestRetry) legacyregistry.RawMustRegister(execPluginCertTTL) legacyregistry.MustRegister(execPluginCertRotation) legacyregistry.MustRegister(execPluginCalls) legacyregistry.MustRegister(transportCacheEntries) legacyregistry.MustRegister(transportCacheCalls) metrics.Register(metrics.RegisterOpts{ ClientCertExpiry: execPluginCertTTLAdapter, ClientCertRotationAge: &rotationAdapter{m: execPluginCertRotation}, RequestLatency: &latencyAdapter{m: requestLatency}, ResolverLatency: &resolverLatencyAdapter{m: resolverLatency}, RequestSize: &sizeAdapter{m: requestSize}, ResponseSize: &sizeAdapter{m: responseSize}, RateLimiterLatency: &latencyAdapter{m: rateLimiterLatency}, RequestResult: &resultAdapter{requestResult}, RequestRetry: &retryAdapter{requestRetry}, ExecPluginCalls: &callsAdapter{m: execPluginCalls}, TransportCacheEntries: &transportCacheAdapter{m: transportCacheEntries}, TransportCreateCalls: &transportCacheCallsAdapter{m: transportCacheCalls}, }) } type latencyAdapter struct { m *k8smetrics.HistogramVec } func (l *latencyAdapter) Observe(ctx context.Context, verb string, u url.URL, latency time.Duration) { l.m.WithContext(ctx).WithLabelValues(verb, u.Host).Observe(latency.Seconds()) } type resolverLatencyAdapter struct { m *k8smetrics.HistogramVec } func (l *resolverLatencyAdapter) Observe(ctx context.Context, host string, latency time.Duration) { l.m.WithContext(ctx).WithLabelValues(host).Observe(latency.Seconds()) } type sizeAdapter struct { m *k8smetrics.HistogramVec } func (s *sizeAdapter) Observe(ctx context.Context, verb string, host string, size float64) { s.m.WithContext(ctx).WithLabelValues(verb, host).Observe(size) } type resultAdapter struct { m *k8smetrics.CounterVec } func (r *resultAdapter) Increment(ctx context.Context, code, method, host string) { r.m.WithContext(ctx).WithLabelValues(code, method, host).Inc() } type expiryToTTLAdapter struct { e *time.Time } func (e *expiryToTTLAdapter) Set(expiry *time.Time) { e.e = expiry } type rotationAdapter struct { m *k8smetrics.Histogram } func (r *rotationAdapter) Observe(d time.Duration) { r.m.Observe(d.Seconds()) } type callsAdapter struct { m *k8smetrics.CounterVec } func (r *callsAdapter) Increment(code int, callStatus string) { r.m.WithLabelValues(fmt.Sprintf("%d", code), callStatus).Inc() } type retryAdapter struct { m *k8smetrics.CounterVec } func (r *retryAdapter) IncrementRetry(ctx context.Context, code, method, host string) { r.m.WithContext(ctx).WithLabelValues(code, method, host).Inc() } type transportCacheAdapter struct { m *k8smetrics.Gauge } func (t *transportCacheAdapter) Observe(value int) { t.m.Set(float64(value)) } type transportCacheCallsAdapter struct { m *k8smetrics.CounterVec } func (t *transportCacheCallsAdapter) Increment(result string) { t.m.WithLabelValues(result).Inc() } kubernetes-component-base-1b2882b/metrics/prometheus/restclient/metrics_test.go000066400000000000000000000055261476422213000302050ustar00rootroot00000000000000/* Copyright 2022 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 restclient import ( "context" "strings" "testing" "k8s.io/client-go/tools/metrics" "k8s.io/component-base/metrics/legacyregistry" "k8s.io/component-base/metrics/testutil" ) func TestClientGOMetrics(t *testing.T) { tests := []struct { description string name string metric interface{} update func() want string }{ { description: "Number of HTTP requests, partitioned by status code, verb, and host.", name: "rest_client_requests_total", metric: requestResult, update: func() { metrics.RequestResult.Increment(context.TODO(), "200", "POST", "www.foo.com") }, want: ` # HELP rest_client_requests_total [ALPHA] Number of HTTP requests, partitioned by status code, method, and host. # TYPE rest_client_requests_total counter rest_client_requests_total{code="200",host="www.foo.com",method="POST"} 1 `, }, { description: "Number of request retries, partitioned by status code, verb, and host.", name: "rest_client_request_retries_total", metric: requestRetry, update: func() { metrics.RequestRetry.IncrementRetry(context.TODO(), "500", "GET", "www.bar.com") }, want: ` # HELP rest_client_request_retries_total [ALPHA] Number of request retries, partitioned by status code, verb, and host. # TYPE rest_client_request_retries_total counter rest_client_request_retries_total{code="500",host="www.bar.com",verb="GET"} 1 `, }, } // no need to register the metrics here, since the init function of // the package registers all the client-go metrics. for _, test := range tests { t.Run(test.description, func(t *testing.T) { resetter, resettable := test.metric.(interface { Reset() }) if !resettable { t.Fatalf("the metric must be resettaable: %s", test.name) } // Since prometheus' gatherer is global, other tests may have updated // metrics already, so we need to reset them prior to running this test. // This also implies that we can't run this test in parallel with other tests. resetter.Reset() test.update() if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(test.want), test.name); err != nil { t.Fatal(err) } }) } } kubernetes-component-base-1b2882b/metrics/prometheus/slis/000077500000000000000000000000001476422213000237375ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/prometheus/slis/metrics.go000066400000000000000000000043171476422213000257410ustar00rootroot00000000000000/* Copyright 2022 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 slis import ( "context" k8smetrics "k8s.io/component-base/metrics" ) type HealthcheckStatus string const ( Success HealthcheckStatus = "success" Error HealthcheckStatus = "error" ) var ( // healthcheck is a Prometheus Gauge metrics used for recording the results of a k8s healthcheck. healthcheck = k8smetrics.NewGaugeVec( &k8smetrics.GaugeOpts{ Namespace: "kubernetes", Name: "healthcheck", Help: "This metric records the result of a single healthcheck.", StabilityLevel: k8smetrics.STABLE, }, []string{"name", "type"}, ) // healthchecksTotal is a Prometheus Counter metrics used for counting the results of a k8s healthcheck. healthchecksTotal = k8smetrics.NewCounterVec( &k8smetrics.CounterOpts{ Namespace: "kubernetes", Name: "healthchecks_total", Help: "This metric records the results of all healthcheck.", StabilityLevel: k8smetrics.STABLE, }, []string{"name", "type", "status"}, ) ) func Register(registry k8smetrics.KubeRegistry) { registry.Register(healthcheck) registry.Register(healthchecksTotal) _ = k8smetrics.RegisterProcessStartTime(registry.Register) } func ResetHealthMetrics() { healthcheck.Reset() healthchecksTotal.Reset() } func ObserveHealthcheck(ctx context.Context, name string, healthcheckType string, status HealthcheckStatus) error { if status == Success { healthcheck.WithContext(ctx).WithLabelValues(name, healthcheckType).Set(1) } else { healthcheck.WithContext(ctx).WithLabelValues(name, healthcheckType).Set(0) } healthchecksTotal.WithContext(ctx).WithLabelValues(name, healthcheckType, string(status)).Inc() return nil } kubernetes-component-base-1b2882b/metrics/prometheus/slis/metrics_test.go000066400000000000000000000060571476422213000270030ustar00rootroot00000000000000/* Copyright 2022 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 slis import ( "context" "strings" "testing" "k8s.io/component-base/metrics" "k8s.io/component-base/metrics/testutil" ) var ( testedMetrics = []string{"kubernetes_healthcheck", "kubernetes_healthchecks_total"} ) func TestObserveHealthcheck(t *testing.T) { registry := metrics.NewKubeRegistry() defer registry.Reset() defer ResetHealthMetrics() Register(registry) initialState := Error healthcheckName := "healthcheck-a" initialOutput := ` # HELP kubernetes_healthcheck [STABLE] This metric records the result of a single healthcheck. # TYPE kubernetes_healthcheck gauge kubernetes_healthcheck{name="healthcheck-a",type="healthz"} 0 # HELP kubernetes_healthchecks_total [STABLE] This metric records the results of all healthcheck. # TYPE kubernetes_healthchecks_total counter kubernetes_healthchecks_total{name="healthcheck-a",status="error",type="healthz"} 1 ` testCases := []struct { desc string name string hcType string hcStatus HealthcheckStatus want string }{ { desc: "test success", name: healthcheckName, hcType: "healthz", hcStatus: Success, want: ` # HELP kubernetes_healthcheck [STABLE] This metric records the result of a single healthcheck. # TYPE kubernetes_healthcheck gauge kubernetes_healthcheck{name="healthcheck-a",type="healthz"} 1 # HELP kubernetes_healthchecks_total [STABLE] This metric records the results of all healthcheck. # TYPE kubernetes_healthchecks_total counter kubernetes_healthchecks_total{name="healthcheck-a",status="error",type="healthz"} 1 kubernetes_healthchecks_total{name="healthcheck-a",status="success",type="healthz"} 1 `, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer ResetHealthMetrics() // let's first record an error as initial state err := ObserveHealthcheck(context.Background(), test.name, test.hcType, initialState) if err != nil { t.Errorf("unexpected err: %v", err) } if err := testutil.GatherAndCompare(registry, strings.NewReader(initialOutput), testedMetrics...); err != nil { t.Fatal(err) } // now record that we successfully purge state err = ObserveHealthcheck(context.Background(), test.name, test.hcType, test.hcStatus) if err != nil { t.Errorf("unexpected err: %v", err) } if err := testutil.GatherAndCompare(registry, strings.NewReader(test.want), testedMetrics...); err != nil { t.Fatal(err) } }) } } kubernetes-component-base-1b2882b/metrics/prometheus/slis/registry.go000066400000000000000000000014151476422213000261370ustar00rootroot00000000000000/* Copyright 2020 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 slis import ( "k8s.io/component-base/metrics" ) var ( // Registry exposes the SLI registry so that additional SLIs can be // added on a per-component basis. Registry = metrics.NewKubeRegistry() ) kubernetes-component-base-1b2882b/metrics/prometheus/slis/routes.go000066400000000000000000000024351476422213000256130ustar00rootroot00000000000000/* Copyright 2020 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 slis import ( "net/http" "sync" "k8s.io/component-base/metrics" ) var ( installOnce = sync.Once{} installWithResetOnce = sync.Once{} ) type mux interface { Handle(path string, handler http.Handler) } type SLIMetrics struct{} // Install adds the DefaultMetrics handler func (s SLIMetrics) Install(m mux) { installOnce.Do(func() { Register(Registry) }) m.Handle("/metrics/slis", metrics.HandlerFor(Registry, metrics.HandlerOpts{})) } type SLIMetricsWithReset struct{} // Install adds the DefaultMetrics handler func (s SLIMetricsWithReset) Install(m mux) { installWithResetOnce.Do(func() { Register(Registry) }) m.Handle("/metrics/slis", metrics.HandlerWithReset(Registry, metrics.HandlerOpts{})) } kubernetes-component-base-1b2882b/metrics/prometheus/slis/routes_test.go000066400000000000000000000022351476422213000266500ustar00rootroot00000000000000/* 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 slis import ( "net/http" "testing" "github.com/stretchr/testify/assert" ) type mockMux struct { handledPaths []string } func (m *mockMux) Handle(path string, handler http.Handler) { m.handledPaths = append(m.handledPaths, path) } func TestSLIMetrics_Install(t *testing.T) { m := &mockMux{} s := SLIMetrics{} s.Install(m) assert.Equal(t, []string{"/metrics/slis"}, m.handledPaths) s.Install(m) // Assert that the path is registered twice for the 2 calls made to Install(). assert.Equal(t, []string{"/metrics/slis", "/metrics/slis"}, m.handledPaths, "Should handle the path twice.") } kubernetes-component-base-1b2882b/metrics/prometheus/version/000077500000000000000000000000001476422213000244525ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/prometheus/version/metrics.go000066400000000000000000000030401476422213000264440ustar00rootroot00000000000000/* Copyright 2019 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 version import ( "k8s.io/component-base/metrics" "k8s.io/component-base/metrics/legacyregistry" "k8s.io/component-base/version" ) var ( buildInfo = metrics.NewGaugeVec( &metrics.GaugeOpts{ Name: "kubernetes_build_info", Help: "A metric with a constant '1' value labeled by major, minor, git version, git commit, git tree state, build date, Go version, and compiler from which Kubernetes was built, and platform on which it is running.", StabilityLevel: metrics.ALPHA, }, []string{"major", "minor", "git_version", "git_commit", "git_tree_state", "build_date", "go_version", "compiler", "platform"}, ) ) // RegisterBuildInfo registers the build and version info in a metadata metric in prometheus func init() { info := version.Get() legacyregistry.MustRegister(buildInfo) buildInfo.WithLabelValues(info.Major, info.Minor, info.GitVersion, info.GitCommit, info.GitTreeState, info.BuildDate, info.GoVersion, info.Compiler, info.Platform).Set(1) } kubernetes-component-base-1b2882b/metrics/prometheus/workqueue/000077500000000000000000000000001476422213000250145ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/prometheus/workqueue/metrics.go000066400000000000000000000113311476422213000270100ustar00rootroot00000000000000/* Copyright 2019 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 workqueue import ( "k8s.io/client-go/util/workqueue" k8smetrics "k8s.io/component-base/metrics" "k8s.io/component-base/metrics/legacyregistry" ) // Package prometheus sets the workqueue DefaultMetricsFactory to produce // prometheus metrics. To use this package, you just have to import it. // Metrics subsystem and keys used by the workqueue. const ( WorkQueueSubsystem = "workqueue" DepthKey = "depth" AddsKey = "adds_total" QueueLatencyKey = "queue_duration_seconds" WorkDurationKey = "work_duration_seconds" UnfinishedWorkKey = "unfinished_work_seconds" LongestRunningProcessorKey = "longest_running_processor_seconds" RetriesKey = "retries_total" ) var ( depth = k8smetrics.NewGaugeVec(&k8smetrics.GaugeOpts{ Subsystem: WorkQueueSubsystem, Name: DepthKey, StabilityLevel: k8smetrics.ALPHA, Help: "Current depth of workqueue", }, []string{"name"}) adds = k8smetrics.NewCounterVec(&k8smetrics.CounterOpts{ Subsystem: WorkQueueSubsystem, Name: AddsKey, StabilityLevel: k8smetrics.ALPHA, Help: "Total number of adds handled by workqueue", }, []string{"name"}) latency = k8smetrics.NewHistogramVec(&k8smetrics.HistogramOpts{ Subsystem: WorkQueueSubsystem, Name: QueueLatencyKey, StabilityLevel: k8smetrics.ALPHA, Help: "How long in seconds an item stays in workqueue before being requested.", Buckets: k8smetrics.ExponentialBuckets(10e-9, 10, 10), }, []string{"name"}) workDuration = k8smetrics.NewHistogramVec(&k8smetrics.HistogramOpts{ Subsystem: WorkQueueSubsystem, Name: WorkDurationKey, StabilityLevel: k8smetrics.ALPHA, Help: "How long in seconds processing an item from workqueue takes.", Buckets: k8smetrics.ExponentialBuckets(10e-9, 10, 10), }, []string{"name"}) unfinished = k8smetrics.NewGaugeVec(&k8smetrics.GaugeOpts{ Subsystem: WorkQueueSubsystem, Name: UnfinishedWorkKey, StabilityLevel: k8smetrics.ALPHA, Help: "How many seconds of work has done that " + "is in progress and hasn't been observed by work_duration. Large " + "values indicate stuck threads. One can deduce the number of stuck " + "threads by observing the rate at which this increases.", }, []string{"name"}) longestRunningProcessor = k8smetrics.NewGaugeVec(&k8smetrics.GaugeOpts{ Subsystem: WorkQueueSubsystem, Name: LongestRunningProcessorKey, StabilityLevel: k8smetrics.ALPHA, Help: "How many seconds has the longest running " + "processor for workqueue been running.", }, []string{"name"}) retries = k8smetrics.NewCounterVec(&k8smetrics.CounterOpts{ Subsystem: WorkQueueSubsystem, Name: RetriesKey, StabilityLevel: k8smetrics.ALPHA, Help: "Total number of retries handled by workqueue", }, []string{"name"}) metrics = []k8smetrics.Registerable{ depth, adds, latency, workDuration, unfinished, longestRunningProcessor, retries, } ) type prometheusMetricsProvider struct { } func init() { for _, m := range metrics { legacyregistry.MustRegister(m) } workqueue.SetProvider(prometheusMetricsProvider{}) } func (prometheusMetricsProvider) NewDepthMetric(name string) workqueue.GaugeMetric { return depth.WithLabelValues(name) } func (prometheusMetricsProvider) NewAddsMetric(name string) workqueue.CounterMetric { return adds.WithLabelValues(name) } func (prometheusMetricsProvider) NewLatencyMetric(name string) workqueue.HistogramMetric { return latency.WithLabelValues(name) } func (prometheusMetricsProvider) NewWorkDurationMetric(name string) workqueue.HistogramMetric { return workDuration.WithLabelValues(name) } func (prometheusMetricsProvider) NewUnfinishedWorkSecondsMetric(name string) workqueue.SettableGaugeMetric { return unfinished.WithLabelValues(name) } func (prometheusMetricsProvider) NewLongestRunningProcessorSecondsMetric(name string) workqueue.SettableGaugeMetric { return longestRunningProcessor.WithLabelValues(name) } func (prometheusMetricsProvider) NewRetriesMetric(name string) workqueue.CounterMetric { return retries.WithLabelValues(name) } kubernetes-component-base-1b2882b/metrics/prometheusextension/000077500000000000000000000000001476422213000247225ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/prometheusextension/timing_histogram.go000066400000000000000000000127041476422213000306210ustar00rootroot00000000000000/* Copyright 2022 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 prometheusextension import ( "errors" "time" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) // GaugeOps is the part of `prometheus.Gauge` that is relevant to // instrumented code. // This factoring should be in prometheus, analogous to the way // it already factors out the Observer interface for histograms and summaries. type GaugeOps interface { // Set is the same as Gauge.Set Set(float64) // Inc is the same as Gauge.inc Inc() // Dec is the same as Gauge.Dec Dec() // Add is the same as Gauge.Add Add(float64) // Sub is the same as Gauge.Sub Sub(float64) // SetToCurrentTime the same as Gauge.SetToCurrentTime SetToCurrentTime() } // A TimingHistogram tracks how long a `float64` variable spends in // ranges defined by buckets. Time is counted in nanoseconds. The // histogram's sum is the integral over time (in nanoseconds, from // creation of the histogram) of the variable's value. type TimingHistogram interface { prometheus.Metric prometheus.Collector GaugeOps } // TimingHistogramOpts is the parameters of the TimingHistogram constructor type TimingHistogramOpts struct { Namespace string Subsystem string Name string Help string ConstLabels prometheus.Labels // Buckets defines the buckets into which observations are // accumulated. Each element in the slice is the upper // inclusive bound of a bucket. The values must be sorted in // strictly increasing order. There is no need to add a // highest bucket with +Inf bound. The default value is // prometheus.DefBuckets. Buckets []float64 // The initial value of the variable. InitialValue float64 } // NewTimingHistogram creates a new TimingHistogram func NewTimingHistogram(opts TimingHistogramOpts) (TimingHistogram, error) { return NewTestableTimingHistogram(time.Now, opts) } // NewTestableTimingHistogram creates a TimingHistogram that uses a mockable clock func NewTestableTimingHistogram(nowFunc func() time.Time, opts TimingHistogramOpts) (TimingHistogram, error) { desc := prometheus.NewDesc( prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), wrapTimingHelp(opts.Help), nil, opts.ConstLabels, ) return newTimingHistogram(nowFunc, desc, opts) } func wrapTimingHelp(given string) string { return "EXPERIMENTAL: " + given } func newTimingHistogram(nowFunc func() time.Time, desc *prometheus.Desc, opts TimingHistogramOpts, variableLabelValues ...string) (TimingHistogram, error) { allLabelsM := prometheus.Labels{} allLabelsS := prometheus.MakeLabelPairs(desc, variableLabelValues) for _, pair := range allLabelsS { if pair == nil || pair.Name == nil || pair.Value == nil { return nil, errors.New("prometheus.MakeLabelPairs returned a nil") } allLabelsM[*pair.Name] = *pair.Value } weighted, err := newWeightedHistogram(desc, WeightedHistogramOpts{ Namespace: opts.Namespace, Subsystem: opts.Subsystem, Name: opts.Name, Help: opts.Help, ConstLabels: allLabelsM, Buckets: opts.Buckets, }, variableLabelValues...) if err != nil { return nil, err } return &timingHistogram{ nowFunc: nowFunc, weighted: weighted, lastSetTime: nowFunc(), value: opts.InitialValue, }, nil } type timingHistogram struct { nowFunc func() time.Time weighted *weightedHistogram // The following fields must only be accessed with weighted's lock held lastSetTime time.Time // identifies when value was last set value float64 } var _ TimingHistogram = &timingHistogram{} func (th *timingHistogram) Set(newValue float64) { th.update(func(float64) float64 { return newValue }) } func (th *timingHistogram) Inc() { th.update(func(oldValue float64) float64 { return oldValue + 1 }) } func (th *timingHistogram) Dec() { th.update(func(oldValue float64) float64 { return oldValue - 1 }) } func (th *timingHistogram) Add(delta float64) { th.update(func(oldValue float64) float64 { return oldValue + delta }) } func (th *timingHistogram) Sub(delta float64) { th.update(func(oldValue float64) float64 { return oldValue - delta }) } func (th *timingHistogram) SetToCurrentTime() { th.update(func(oldValue float64) float64 { return th.nowFunc().Sub(time.Unix(0, 0)).Seconds() }) } func (th *timingHistogram) update(updateFn func(float64) float64) { th.weighted.lock.Lock() defer th.weighted.lock.Unlock() now := th.nowFunc() delta := now.Sub(th.lastSetTime) value := th.value if delta > 0 { th.weighted.observeWithWeightLocked(value, uint64(delta)) th.lastSetTime = now } th.value = updateFn(value) } func (th *timingHistogram) Desc() *prometheus.Desc { return th.weighted.Desc() } func (th *timingHistogram) Write(dest *dto.Metric) error { th.Add(0) // account for time since last update return th.weighted.Write(dest) } func (th *timingHistogram) Describe(ch chan<- *prometheus.Desc) { ch <- th.weighted.Desc() } func (th *timingHistogram) Collect(ch chan<- prometheus.Metric) { ch <- th } kubernetes-component-base-1b2882b/metrics/prometheusextension/timing_histogram_test.go000066400000000000000000000171461476422213000316650ustar00rootroot00000000000000/* Copyright 2022 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 prometheusextension import ( "math" "testing" "time" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) func TestTimingHistogramNonMonotonicBuckets(t *testing.T) { testCases := map[string][]float64{ "not strictly monotonic": {1, 2, 2, 3}, "not monotonic at all": {1, 2, 4, 3, 5}, "have +Inf in the middle": {1, 2, math.Inf(+1), 3}, } for name, buckets := range testCases { _, err := NewTimingHistogram(TimingHistogramOpts{ Name: "test_histogram", Help: "helpless", Buckets: buckets, }) if err == nil { t.Errorf("Buckets %v are %s but NewHTimingistogram did not complain.", buckets, name) } } } func exerciseTimingHistogramAndCollector(th GaugeOps, t0 time.Time, clk *unsyncFakeClock, collect func(chan<- prometheus.Metric), expectCollection ...GaugeOps) func(t *testing.T) { return func(t *testing.T) { exerciseTimingHistogramData(t, th, t0, clk) exerciseTimingHistogramCollector(t, collect, expectCollection) } } func exerciseTimingHistogramCollector(t *testing.T, collect func(chan<- prometheus.Metric), expectCollection []GaugeOps) { remainingCollection := expectCollection metch := make(chan prometheus.Metric) go func() { collect(metch) close(metch) }() for collected := range metch { collectedGO := collected.(GaugeOps) newRem, found := findAndRemove(remainingCollection, collectedGO) if !found { t.Errorf("Collected unexpected value %#+v", collected) } remainingCollection = newRem } if len(remainingCollection) > 0 { t.Errorf("Collection omitted %#+v", remainingCollection) } } var thTestBuckets = []float64{0, 0.5, 1} var thTestV0 float64 = 0.25 // exerciseTimingHistogramData takes the given histogram through the following points in (time,value) space. // t0 is the clock time of the histogram's construction // value=v0 for t0 <= t <= t1 where v0 = 0.25 and t1 = t0 + 1 ns // value=v1 for t1 <= t <= t2 where v1 = 0.75 and t2 = t1 + 1 microsecond // value=v2 for t2 <= t <= t3 where v2 = 1.25 and t3 = t2 + 1 millisecond // value=v3 for t3 <= t <= t4 where v3 = 0.65 and t4 = t3 + 1 second func exerciseTimingHistogramData(t *testing.T, th GaugeOps, t0 time.Time, clk *unsyncFakeClock) { t1 := t0.Add(time.Nanosecond) v0 := thTestV0 var v1 float64 = 0.75 clk.SetTime(t1) th.Set(v1) t2 := t1.Add(time.Microsecond) var d2 float64 = 0.5 v2 := v1 + d2 clk.SetTime(t2) th.Add(d2) t3 := t2 for i := 0; i < 1000000; i++ { t3 = t3.Add(time.Nanosecond) clk.SetTime(t3) th.Set(v2) } var d3 float64 = -0.6 v3 := v2 + d3 th.Add(d3) t4 := t3.Add(time.Second) clk.SetTime(t4) metric := &dto.Metric{} writer := th.(prometheus.Metric) err := writer.Write(metric) if err != nil { t.Error(err) } wroteHist := metric.Histogram if want, got := uint64(t4.Sub(t0)), wroteHist.GetSampleCount(); want != got { t.Errorf("Wanted %v but got %v", want, got) } if want, got := tDiff(t1, t0)*v0+tDiff(t2, t1)*v1+tDiff(t3, t2)*v2+tDiff(t4, t3)*v3, wroteHist.GetSampleSum(); want != got { t.Errorf("Wanted %v but got %v", want, got) } wroteBuckets := wroteHist.GetBucket() if len(wroteBuckets) != len(thTestBuckets) { t.Errorf("Got buckets %#+v", wroteBuckets) } expectedCounts := []time.Duration{0, t1.Sub(t0), t2.Sub(t0) + t4.Sub(t3)} for idx, ub := range thTestBuckets { if want, got := uint64(expectedCounts[idx]), wroteBuckets[idx].GetCumulativeCount(); want != got { t.Errorf("In bucket %d, wanted %v but got %v", idx, want, got) } if want, got := ub, wroteBuckets[idx].GetUpperBound(); want != got { t.Errorf("In bucket %d, wanted %v but got %v", idx, want, got) } } } // tDiff returns a time difference as float func tDiff(hi, lo time.Time) float64 { return float64(hi.Sub(lo)) } func findAndRemove(metrics []GaugeOps, seek GaugeOps) ([]GaugeOps, bool) { for idx, metric := range metrics { if metric == seek { return append(append([]GaugeOps{}, metrics[:idx]...), metrics[idx+1:]...), true } } return metrics, false } func TestTimeIntegrationDirect(t *testing.T) { t0 := time.Now() clk := &unsyncFakeClock{t0} th, err := NewTestableTimingHistogram(clk.Now, TimingHistogramOpts{ Name: "TestTimeIntegration", Help: "helpless", Buckets: thTestBuckets, InitialValue: thTestV0, }) if err != nil { t.Error(err) return } t.Run("non-vec", exerciseTimingHistogramAndCollector(th, t0, clk, th.Collect, th)) } func TestTimingHistogramVec(t *testing.T) { t0 := time.Now() clk := &unsyncFakeClock{t0} vec := NewTestableTimingHistogramVec(clk.Now, TimingHistogramOpts{ Name: "TestTimeIntegration", Help: "helpless", Buckets: thTestBuckets, InitialValue: thTestV0, }, "k1", "k2") th1 := vec.With(prometheus.Labels{"k1": "a", "k2": "x"}) th1b := vec.WithLabelValues("a", "x") if th1 != th1b { t.Errorf("Vector not functional") } t.Run("th1", exerciseTimingHistogramAndCollector(th1, t0, clk, vec.Collect, th1)) t0 = clk.Now() th2 := vec.WithLabelValues("a", "y") if th1 == th2 { t.Errorf("Vector does not distinguish label values") } t.Run("th2", exerciseTimingHistogramAndCollector(th2, t0, clk, vec.Collect, th1, th2)) t0 = clk.Now() th3 := vec.WithLabelValues("b", "y") if th1 == th3 || th2 == th3 { t.Errorf("Vector does not distinguish label values") } t.Run("th2", exerciseTimingHistogramAndCollector(th3, t0, clk, vec.Collect, th1, th2, th3)) } type unsyncFakeClock struct { now time.Time } func (ufc *unsyncFakeClock) Now() time.Time { return ufc.now } func (ufc *unsyncFakeClock) SetTime(now time.Time) { ufc.now = now } func BenchmarkTimingHistogramDirect(b *testing.B) { b.StopTimer() now := time.Now() hist, err := NewTestableTimingHistogram(func() time.Time { return now }, TimingHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "testhist", Help: "Me", Buckets: []float64{1, 2, 4, 8, 16}, }) if err != nil { b.Error(err) } var x int b.StartTimer() for i := 0; i < b.N; i++ { now = now.Add(time.Duration(31-x) * time.Microsecond) hist.Set(float64(x)) x = (x + i) % 23 } } func BenchmarkTimingHistogramVecEltCached(b *testing.B) { b.StopTimer() now := time.Now() vec := NewTestableTimingHistogramVec(func() time.Time { return now }, TimingHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "testhist", Help: "Me", Buckets: []float64{1, 2, 4, 8, 16}, }, "label1", "label2") hist := vec.WithLabelValues("val1", "val2") var x int b.StartTimer() for i := 0; i < b.N; i++ { now = now.Add(time.Duration(31-x) * time.Microsecond) hist.Set(float64(x)) x = (x + i) % 23 } } func BenchmarkTimingHistogramVecEltFetched(b *testing.B) { b.StopTimer() now := time.Now() vec := NewTestableTimingHistogramVec(func() time.Time { return now }, TimingHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "testhist", Help: "Me", Buckets: []float64{1, 2, 4, 8, 16}, }, "label1", "label2") var x int b.StartTimer() for i := 0; i < b.N; i++ { now = now.Add(time.Duration(31-x) * time.Microsecond) vec.WithLabelValues("val1", "val2").Set(float64(x)) x = (x + i) % 23 } } kubernetes-component-base-1b2882b/metrics/prometheusextension/timing_histogram_vec.go000066400000000000000000000061231476422213000314540ustar00rootroot00000000000000/* Copyright 2022 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 prometheusextension import ( "time" "github.com/prometheus/client_golang/prometheus" ) // GaugeVecOps is a bunch of Gauge that have the same // Desc and are distinguished by the values for their variable labels. type GaugeVecOps interface { GetMetricWith(prometheus.Labels) (GaugeOps, error) GetMetricWithLabelValues(lvs ...string) (GaugeOps, error) With(prometheus.Labels) GaugeOps WithLabelValues(...string) GaugeOps CurryWith(prometheus.Labels) (GaugeVecOps, error) MustCurryWith(prometheus.Labels) GaugeVecOps } type TimingHistogramVec struct { *prometheus.MetricVec } var _ GaugeVecOps = &TimingHistogramVec{} var _ prometheus.Collector = &TimingHistogramVec{} func NewTimingHistogramVec(opts TimingHistogramOpts, labelNames ...string) *TimingHistogramVec { return NewTestableTimingHistogramVec(time.Now, opts, labelNames...) } func NewTestableTimingHistogramVec(nowFunc func() time.Time, opts TimingHistogramOpts, labelNames ...string) *TimingHistogramVec { desc := prometheus.NewDesc( prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), wrapTimingHelp(opts.Help), labelNames, opts.ConstLabels, ) return &TimingHistogramVec{ MetricVec: prometheus.NewMetricVec(desc, func(lvs ...string) prometheus.Metric { metric, err := newTimingHistogram(nowFunc, desc, opts, lvs...) if err != nil { panic(err) // like in prometheus.newHistogram } return metric }), } } func (hv *TimingHistogramVec) GetMetricWith(labels prometheus.Labels) (GaugeOps, error) { metric, err := hv.MetricVec.GetMetricWith(labels) if metric != nil { return metric.(GaugeOps), err } return nil, err } func (hv *TimingHistogramVec) GetMetricWithLabelValues(lvs ...string) (GaugeOps, error) { metric, err := hv.MetricVec.GetMetricWithLabelValues(lvs...) if metric != nil { return metric.(GaugeOps), err } return nil, err } func (hv *TimingHistogramVec) With(labels prometheus.Labels) GaugeOps { h, err := hv.GetMetricWith(labels) if err != nil { panic(err) } return h } func (hv *TimingHistogramVec) WithLabelValues(lvs ...string) GaugeOps { h, err := hv.GetMetricWithLabelValues(lvs...) if err != nil { panic(err) } return h } func (hv *TimingHistogramVec) CurryWith(labels prometheus.Labels) (GaugeVecOps, error) { vec, err := hv.MetricVec.CurryWith(labels) if vec != nil { return &TimingHistogramVec{MetricVec: vec}, err } return nil, err } func (hv *TimingHistogramVec) MustCurryWith(labels prometheus.Labels) GaugeVecOps { vec, err := hv.CurryWith(labels) if err != nil { panic(err) } return vec } kubernetes-component-base-1b2882b/metrics/prometheusextension/weighted_histogram.go000066400000000000000000000143131476422213000311300ustar00rootroot00000000000000/* Copyright 2022 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 prometheusextension import ( "fmt" "math" "sort" "sync" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) // WeightedHistogram generalizes Histogram: each observation has // an associated _weight_. For a given `x` and `N`, // `1` call on `ObserveWithWeight(x, N)` has the same meaning as // `N` calls on `ObserveWithWeight(x, 1)`. // The weighted sum might differ slightly due to the use of // floating point, although the implementation takes some steps // to mitigate that. // If every weight were 1, // this would be the same as the existing Histogram abstraction. type WeightedHistogram interface { prometheus.Metric prometheus.Collector WeightedObserver } // WeightedObserver generalizes the Observer interface. type WeightedObserver interface { // Set the variable to the given value with the given weight. ObserveWithWeight(value float64, weight uint64) } // WeightedHistogramOpts is the same as for an ordinary Histogram type WeightedHistogramOpts = prometheus.HistogramOpts // NewWeightedHistogram creates a new WeightedHistogram func NewWeightedHistogram(opts WeightedHistogramOpts) (WeightedHistogram, error) { desc := prometheus.NewDesc( prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), wrapWeightedHelp(opts.Help), nil, opts.ConstLabels, ) return newWeightedHistogram(desc, opts) } func wrapWeightedHelp(given string) string { return "EXPERIMENTAL: " + given } func newWeightedHistogram(desc *prometheus.Desc, opts WeightedHistogramOpts, variableLabelValues ...string) (*weightedHistogram, error) { if len(opts.Buckets) == 0 { opts.Buckets = prometheus.DefBuckets } for i, upperBound := range opts.Buckets { if i < len(opts.Buckets)-1 { if upperBound >= opts.Buckets[i+1] { return nil, fmt.Errorf( "histogram buckets must be in increasing order: %f >= %f", upperBound, opts.Buckets[i+1], ) } } else { if math.IsInf(upperBound, +1) { // The +Inf bucket is implicit. Remove it here. opts.Buckets = opts.Buckets[:i] } } } upperBounds := make([]float64, len(opts.Buckets)) copy(upperBounds, opts.Buckets) return &weightedHistogram{ desc: desc, variableLabelValues: variableLabelValues, upperBounds: upperBounds, buckets: make([]uint64, len(upperBounds)+1), hotCount: initialHotCount, }, nil } type weightedHistogram struct { desc *prometheus.Desc variableLabelValues []string upperBounds []float64 // exclusive of +Inf lock sync.Mutex // applies to all the following // buckets is longer by one than upperBounds. // For 0 <= idx < len(upperBounds), buckets[idx] holds the // accumulated time.Duration that value has been <= // upperBounds[idx] but not <= upperBounds[idx-1]. // buckets[len(upperBounds)] holds the accumulated // time.Duration when value fit in no other bucket. buckets []uint64 // sumHot + sumCold is the weighted sum of value. // Rather than risk loss of precision in one // float64, we do this sum hierarchically. Many successive // increments are added into sumHot; once in a while // the magnitude of sumHot is compared to the magnitude // of sumCold and, if the ratio is high enough, // sumHot is transferred into sumCold. sumHot float64 sumCold float64 transferThreshold float64 // = math.Abs(sumCold) / 2^26 (that's about half of the bits of precision in a float64) // hotCount is used to decide when to consider dumping sumHot into sumCold. // hotCount counts upward from initialHotCount to zero. hotCount int } // initialHotCount is the negative of the number of terms // that are summed into sumHot before considering whether // to transfer to sumCold. This only has to be big enough // to make the extra floating point operations occur in a // distinct minority of cases. const initialHotCount = -15 var _ WeightedHistogram = &weightedHistogram{} var _ prometheus.Metric = &weightedHistogram{} var _ prometheus.Collector = &weightedHistogram{} func (sh *weightedHistogram) ObserveWithWeight(value float64, weight uint64) { idx := sort.SearchFloat64s(sh.upperBounds, value) sh.lock.Lock() defer sh.lock.Unlock() sh.updateLocked(idx, value, weight) } func (sh *weightedHistogram) observeWithWeightLocked(value float64, weight uint64) { idx := sort.SearchFloat64s(sh.upperBounds, value) sh.updateLocked(idx, value, weight) } func (sh *weightedHistogram) updateLocked(idx int, value float64, weight uint64) { sh.buckets[idx] += weight newSumHot := sh.sumHot + float64(weight)*value sh.hotCount++ if sh.hotCount >= 0 { sh.hotCount = initialHotCount if math.Abs(newSumHot) > sh.transferThreshold { newSumCold := sh.sumCold + newSumHot sh.sumCold = newSumCold sh.transferThreshold = math.Abs(newSumCold / 67108864) sh.sumHot = 0 return } } sh.sumHot = newSumHot } func (sh *weightedHistogram) Desc() *prometheus.Desc { return sh.desc } func (sh *weightedHistogram) Write(dest *dto.Metric) error { count, sum, buckets := func() (uint64, float64, map[float64]uint64) { sh.lock.Lock() defer sh.lock.Unlock() nBounds := len(sh.upperBounds) buckets := make(map[float64]uint64, nBounds) var count uint64 for idx, upperBound := range sh.upperBounds { count += sh.buckets[idx] buckets[upperBound] = count } count += sh.buckets[nBounds] return count, sh.sumHot + sh.sumCold, buckets }() metric, err := prometheus.NewConstHistogram(sh.desc, count, sum, buckets, sh.variableLabelValues...) if err != nil { return err } return metric.Write(dest) } func (sh *weightedHistogram) Describe(ch chan<- *prometheus.Desc) { ch <- sh.desc } func (sh *weightedHistogram) Collect(ch chan<- prometheus.Metric) { ch <- sh } kubernetes-component-base-1b2882b/metrics/prometheusextension/weighted_histogram_test.go000066400000000000000000000227701476422213000321750ustar00rootroot00000000000000/* Copyright 2022 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 prometheusextension import ( "fmt" "math" "math/rand" "sort" "testing" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) // Float64Slice is a slice of float64 that sorts by magnitude type Float64Slice []float64 func (fs Float64Slice) Len() int { return len(fs) } func (fs Float64Slice) Less(i, j int) bool { return math.Abs(fs[i]) < math.Abs(fs[j]) } func (fs Float64Slice) Swap(i, j int) { fs[i], fs[j] = fs[j], fs[i] } // weightedHistogramSpecFunc returns a WeightedHistogram and the upper bounds // to expect it to have. // Every invocation of the same function returns the same histogram. type weightedHistogramSpecFunc func() (wh WeightedObserver, upperBounds []float64) // exerciseWeightedHistograms exercises a given collection of WeightedHistograms. // Each histogram is given by a function that returns it, so that we can test // that the Vec functions return the same result for the same input. // For each histogram, with N upper bounds, the exercise provides two 2N+1 values: // the upper bounds and values halfway between them (extended below the bottom and above // the top). For the Jth value, there are J*m1 calls to ObserveWithWeight with m1 // chosen so that m1 * sum[1 <= J <= 2N+1] J is large enough to trigger several // considerations of spilling from sumHot to sumCold. // The ObserveWithWeight calls to the various histograms are interleaved to check // that there is no interference between them. func exerciseWeightedHistograms(t *testing.T, whSpecs ...weightedHistogramSpecFunc) { var whos []weightedHistogramObs expectations := []whExerciseExpectation{} // Create expectations and specs of calls ot ObserveWithWeight for whIdx, whSpec := range whSpecs { wh, upperBounds := whSpec() numUBs := len(upperBounds) numWhos := numUBs*2 + 1 multSum := (numWhos * (numWhos + 1)) / 2 m1 := (-10 * initialHotCount) / multSum terms := Float64Slice{} ee := whExerciseExpectation{wh: wh, upperBounds: upperBounds, buckets: make([]uint64, numUBs), } addWHOs := func(val float64, weight uint64, mult, idx int) { multipliedWeight := weight * uint64(mult) terms = append(terms, val*float64(multipliedWeight)) t.Logf("For WH %d, adding obs val=%v, weight=%v, mult=%d, idx=%d", whIdx, val, weight, mult, idx) for i := 0; i < mult; i++ { whos = append(whos, weightedHistogramObs{whSpec, val, weight}) } for j := idx; j < numUBs; j++ { ee.buckets[j] += multipliedWeight } ee.count += multipliedWeight } for idx, ub := range upperBounds { var val float64 if idx > 0 { val = (upperBounds[idx-1] + ub) / 2 } else if numUBs > 1 { val = (3*ub - upperBounds[1]) / 2 } else { val = ub - 1 } addWHOs(val, (1 << rand.Intn(40)), (2*idx+1)*m1, idx) addWHOs(ub, (1 << rand.Intn(40)), (2*idx+2)*m1, idx) } val := upperBounds[numUBs-1] + 1 if numUBs > 1 { val = (3*upperBounds[numUBs-1] - upperBounds[numUBs-2]) / 2 } addWHOs(val, 1+uint64(rand.Intn(1000000)), (2*numUBs+1)*m1, numUBs) sort.Sort(terms) for _, term := range terms { ee.sum += term } t.Logf("At idx=%v, adding expectation of buckets=%#+v, upperBounds=%#+v, sum=%v, count=%v", whIdx, ee.buckets, ee.upperBounds, ee.sum, ee.count) expectations = append(expectations, ee) } // Do the planned calls on ObserveWithWeight, in randomized order for len(whos) > 0 { var wi weightedHistogramObs whos, wi = whosPick(whos) wh, _ := wi.whSpec() wh.ObserveWithWeight(wi.val, wi.weight) // t.Logf("ObserveWithWeight(%v, %v) => %#+v", wi.val, wi.weight, wh) } // Check expectations for idx, ee := range expectations { wh := ee.wh whAsMetric := wh.(prometheus.Metric) var metric dto.Metric whAsMetric.Write(&metric) actualHist := metric.GetHistogram() if actualHist == nil { t.Errorf("At idx=%d, Write produced nil Histogram", idx) } actualCount := actualHist.GetSampleCount() if actualCount != ee.count { t.Errorf("At idx=%d, expected count %v but got %v", idx, ee.count, actualCount) } actualBuckets := actualHist.GetBucket() if len(ee.buckets) != len(actualBuckets) { t.Errorf("At idx=%d, expected %v buckets but got %v", idx, len(ee.buckets), len(actualBuckets)) } for j := 0; j < len(ee.buckets) && j < len(actualBuckets); j++ { actualUB := actualBuckets[j].GetUpperBound() actualCount := actualBuckets[j].GetCumulativeCount() if ee.upperBounds[j] != actualUB { t.Errorf("At idx=%d, bucket %d, expected upper bound %v but got %v, err=%v", idx, j, ee.upperBounds[j], actualUB, actualUB-ee.upperBounds[j]) } if ee.buckets[j] != actualCount { t.Errorf("At idx=%d, bucket %d expected count %d but got %d", idx, j, ee.buckets[j], actualCount) } } actualSum := actualHist.GetSampleSum() num := math.Abs(actualSum - ee.sum) den := math.Max(math.Abs(actualSum), math.Abs(ee.sum)) relErr := num / den // Issue 120112 reports relative errors as high as 9.55994394104272e-14 if relErr > 1e-13 { t.Errorf("At idx=%d, expected sum %v but got %v, err=%v, relativeErr=%v", idx, ee.sum, actualSum, actualSum-ee.sum, relErr) } } } // weightedHistogramObs prescribes a call on WeightedHistogram::ObserveWithWeight type weightedHistogramObs struct { whSpec weightedHistogramSpecFunc val float64 weight uint64 } // whExerciseExpectation is the expected result from exercising a WeightedHistogram type whExerciseExpectation struct { wh WeightedObserver upperBounds []float64 buckets []uint64 sum float64 count uint64 } func whosPick(whos []weightedHistogramObs) ([]weightedHistogramObs, weightedHistogramObs) { n := len(whos) if n < 2 { return whos[:0], whos[0] } idx := rand.Intn(n) ans := whos[idx] whos[idx] = whos[n-1] return whos[:n-1], ans } func TestOneWeightedHistogram(t *testing.T) { // First, some literal test cases for _, testCase := range []struct { name string upperBounds []float64 }{ {"one bucket", []float64{0.07}}, {"two buckets", []float64{0.07, 0.13}}, {"three buckets", []float64{0.07, 0.13, 1e6}}, } { t.Run(testCase.name, func(t *testing.T) { wh, err := NewWeightedHistogram(WeightedHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "testhist", Help: "Me", Buckets: testCase.upperBounds, }) if err != nil { t.Error(err) } exerciseWeightedHistograms(t, func() (WeightedObserver, []float64) { return wh, testCase.upperBounds }) }) } // Now, some randomized test cases for i := 0; i < 10; i++ { name := fmt.Sprintf("random_case_%d", i) t.Run(name, func(t *testing.T) { nBounds := rand.Intn(10) + 1 ubs := []float64{} var bound float64 for j := 0; j < nBounds; j++ { bound += rand.Float64() ubs = append(ubs, bound) } wh, err := NewWeightedHistogram(WeightedHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: name, Help: "Me", Buckets: ubs, ConstLabels: prometheus.Labels{"k0": "v0"}, }) if err != nil { t.Error(err) } exerciseWeightedHistograms(t, func() (WeightedObserver, []float64) { return wh, ubs }) }) } } func TestWeightedHistogramVec(t *testing.T) { ubs1 := []float64{0.07, 1.3, 1e6} vec1 := NewWeightedHistogramVec(WeightedHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "vec1", Help: "Me", Buckets: ubs1, ConstLabels: prometheus.Labels{"k0": "v0"}, }, "k1", "k2") gen1 := func(lvs ...string) func() (WeightedObserver, []float64) { return func() (WeightedObserver, []float64) { return vec1.WithLabelValues(lvs...), ubs1 } } ubs2 := []float64{-0.03, 0.71, 1e9} vec2 := NewWeightedHistogramVec(WeightedHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "vec2", Help: "Me", Buckets: ubs2, ConstLabels: prometheus.Labels{"j0": "u0"}, }, "j1", "j2") gen2 := func(lvs ...string) func() (WeightedObserver, []float64) { varLabels := prometheus.Labels{} varLabels["j1"] = lvs[0] varLabels["j2"] = lvs[1] return func() (WeightedObserver, []float64) { return vec2.With(varLabels), ubs2 } } exerciseWeightedHistograms(t, gen1("v11", "v21"), gen1("v12", "v21"), gen1("v12", "v22"), gen2("a", "b"), gen2("a", "c"), gen2("b", "c"), ) } func BenchmarkWeightedHistogram(b *testing.B) { b.StopTimer() wh, err := NewWeightedHistogram(WeightedHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "testhist", Help: "Me", Buckets: []float64{1, 2, 4, 8, 16}, }) if err != nil { b.Error(err) } var x int b.StartTimer() for i := 0; i < b.N; i++ { wh.ObserveWithWeight(float64(x), uint64(i)%32+1) x = (x + i) % 20 } } func BenchmarkHistogram(b *testing.B) { b.StopTimer() hist := prometheus.NewHistogram(prometheus.HistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "testhist", Help: "Me", Buckets: []float64{1, 2, 4, 8, 16}, }) var x int b.StartTimer() for i := 0; i < b.N; i++ { hist.Observe(float64(x)) x = (x + i) % 20 } } kubernetes-component-base-1b2882b/metrics/prometheusextension/weighted_histogram_vec.go000066400000000000000000000061411476422213000317650ustar00rootroot00000000000000/* Copyright 2022 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 prometheusextension import ( "github.com/prometheus/client_golang/prometheus" ) // WeightedObserverVec is a bunch of WeightedObservers that have the same // Desc and are distinguished by the values for their variable labels. type WeightedObserverVec interface { GetMetricWith(prometheus.Labels) (WeightedObserver, error) GetMetricWithLabelValues(lvs ...string) (WeightedObserver, error) With(prometheus.Labels) WeightedObserver WithLabelValues(...string) WeightedObserver CurryWith(prometheus.Labels) (WeightedObserverVec, error) MustCurryWith(prometheus.Labels) WeightedObserverVec } // WeightedHistogramVec implements WeightedObserverVec type WeightedHistogramVec struct { *prometheus.MetricVec } var _ WeightedObserverVec = &WeightedHistogramVec{} var _ prometheus.Collector = &WeightedHistogramVec{} func NewWeightedHistogramVec(opts WeightedHistogramOpts, labelNames ...string) *WeightedHistogramVec { desc := prometheus.NewDesc( prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), wrapWeightedHelp(opts.Help), labelNames, opts.ConstLabels, ) return &WeightedHistogramVec{ MetricVec: prometheus.NewMetricVec(desc, func(lvs ...string) prometheus.Metric { metric, err := newWeightedHistogram(desc, opts, lvs...) if err != nil { panic(err) // like in prometheus.newHistogram } return metric }), } } func (hv *WeightedHistogramVec) GetMetricWith(labels prometheus.Labels) (WeightedObserver, error) { metric, err := hv.MetricVec.GetMetricWith(labels) if metric != nil { return metric.(WeightedObserver), err } return nil, err } func (hv *WeightedHistogramVec) GetMetricWithLabelValues(lvs ...string) (WeightedObserver, error) { metric, err := hv.MetricVec.GetMetricWithLabelValues(lvs...) if metric != nil { return metric.(WeightedObserver), err } return nil, err } func (hv *WeightedHistogramVec) With(labels prometheus.Labels) WeightedObserver { h, err := hv.GetMetricWith(labels) if err != nil { panic(err) } return h } func (hv *WeightedHistogramVec) WithLabelValues(lvs ...string) WeightedObserver { h, err := hv.GetMetricWithLabelValues(lvs...) if err != nil { panic(err) } return h } func (hv *WeightedHistogramVec) CurryWith(labels prometheus.Labels) (WeightedObserverVec, error) { vec, err := hv.MetricVec.CurryWith(labels) if vec != nil { return &WeightedHistogramVec{MetricVec: vec}, err } return nil, err } func (hv *WeightedHistogramVec) MustCurryWith(labels prometheus.Labels) WeightedObserverVec { vec, err := hv.CurryWith(labels) if err != nil { panic(err) } return vec } kubernetes-component-base-1b2882b/metrics/registry.go000066400000000000000000000303151476422213000227730ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "fmt" "sync" "sync/atomic" "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" apimachineryversion "k8s.io/apimachinery/pkg/version" "k8s.io/component-base/version" ) var ( showHiddenOnce sync.Once disabledMetricsLock sync.RWMutex showHidden atomic.Bool registries []*kubeRegistry // stores all registries created by NewKubeRegistry() registriesLock sync.RWMutex disabledMetrics = map[string]struct{}{} registeredMetricsTotal = NewCounterVec( &CounterOpts{ Name: "registered_metrics_total", Help: "The count of registered metrics broken by stability level and deprecation version.", StabilityLevel: BETA, }, []string{"stability_level", "deprecated_version"}, ) disabledMetricsTotal = NewCounter( &CounterOpts{ Name: "disabled_metrics_total", Help: "The count of disabled metrics.", StabilityLevel: BETA, }, ) hiddenMetricsTotal = NewCounter( &CounterOpts{ Name: "hidden_metrics_total", Help: "The count of hidden metrics.", StabilityLevel: BETA, }, ) cardinalityEnforcementUnexpectedCategorizationsTotal = NewCounter( &CounterOpts{ Name: "cardinality_enforcement_unexpected_categorizations_total", Help: "The count of unexpected categorizations during cardinality enforcement.", StabilityLevel: ALPHA, }, ) ) // shouldHide be used to check if a specific metric with deprecated version should be hidden // according to metrics deprecation lifecycle. func shouldHide(currentVersion *semver.Version, deprecatedVersion *semver.Version) bool { guardVersion, err := semver.Make(fmt.Sprintf("%d.%d.0", currentVersion.Major, currentVersion.Minor)) if err != nil { panic("failed to make version from current version") } if deprecatedVersion.LT(guardVersion) { return true } return false } // ValidateShowHiddenMetricsVersion checks invalid version for which show hidden metrics. func ValidateShowHiddenMetricsVersion(v string) []error { err := validateShowHiddenMetricsVersion(parseVersion(version.Get()), v) if err != nil { return []error{err} } return nil } func SetDisabledMetric(name string) { disabledMetricsLock.Lock() defer disabledMetricsLock.Unlock() disabledMetrics[name] = struct{}{} disabledMetricsTotal.Inc() } // SetShowHidden will enable showing hidden metrics. This will no-opt // after the initial call func SetShowHidden() { showHiddenOnce.Do(func() { showHidden.Store(true) // re-register collectors that has been hidden in phase of last registry. for _, r := range registries { r.enableHiddenCollectors() r.enableHiddenStableCollectors() } }) } // ShouldShowHidden returns whether showing hidden deprecated metrics // is enabled. While the primary usecase for this is internal (to determine // registration behavior) this can also be used to introspect func ShouldShowHidden() bool { return showHidden.Load() } // Registerable is an interface for a collector metric which we // will register with KubeRegistry. type Registerable interface { prometheus.Collector // Create will mark deprecated state for the collector Create(version *semver.Version) bool // ClearState will clear all the states marked by Create. ClearState() // FQName returns the fully-qualified metric name of the collector. FQName() string } type resettable interface { Reset() } // KubeRegistry is an interface which implements a subset of prometheus.Registerer and // prometheus.Gatherer interfaces type KubeRegistry interface { // Deprecated RawMustRegister(...prometheus.Collector) // CustomRegister is our internal variant of Prometheus registry.Register CustomRegister(c StableCollector) error // CustomMustRegister is our internal variant of Prometheus registry.MustRegister CustomMustRegister(cs ...StableCollector) // Register conforms to Prometheus registry.Register Register(Registerable) error // MustRegister conforms to Prometheus registry.MustRegister MustRegister(...Registerable) // Unregister conforms to Prometheus registry.Unregister Unregister(collector Collector) bool // Gather conforms to Prometheus gatherer.Gather Gather() ([]*dto.MetricFamily, error) // Reset invokes the Reset() function on all items in the registry // which are added as resettables. Reset() // RegisterMetaMetrics registers metrics about the number of registered metrics. RegisterMetaMetrics() // Registerer exposes the underlying prometheus registerer Registerer() prometheus.Registerer // Gatherer exposes the underlying prometheus gatherer Gatherer() prometheus.Gatherer } // kubeRegistry is a wrapper around a prometheus registry-type object. Upon initialization // the kubernetes binary version information is loaded into the registry object, so that // automatic behavior can be configured for metric versioning. type kubeRegistry struct { PromRegistry version semver.Version hiddenCollectors map[string]Registerable // stores all collectors that has been hidden stableCollectors []StableCollector // stores all stable collector hiddenCollectorsLock sync.RWMutex stableCollectorsLock sync.RWMutex resetLock sync.RWMutex resettables []resettable } // Register registers a new Collector to be included in metrics // collection. It returns an error if the descriptors provided by the // Collector are invalid or if they — in combination with descriptors of // already registered Collectors — do not fulfill the consistency and // uniqueness criteria described in the documentation of metric.Desc. func (kr *kubeRegistry) Register(c Registerable) error { if c.Create(&kr.version) { defer kr.addResettable(c) return kr.PromRegistry.Register(c) } kr.trackHiddenCollector(c) return nil } // Registerer exposes the underlying prometheus.Registerer func (kr *kubeRegistry) Registerer() prometheus.Registerer { return kr.PromRegistry } // Gatherer exposes the underlying prometheus.Gatherer func (kr *kubeRegistry) Gatherer() prometheus.Gatherer { return kr.PromRegistry } // MustRegister works like Register but registers any number of // Collectors and panics upon the first registration that causes an // error. func (kr *kubeRegistry) MustRegister(cs ...Registerable) { metrics := make([]prometheus.Collector, 0, len(cs)) for _, c := range cs { if c.Create(&kr.version) { metrics = append(metrics, c) kr.addResettable(c) } else { kr.trackHiddenCollector(c) } } kr.PromRegistry.MustRegister(metrics...) } // CustomRegister registers a new custom collector. func (kr *kubeRegistry) CustomRegister(c StableCollector) error { kr.trackStableCollectors(c) defer kr.addResettable(c) if c.Create(&kr.version, c) { return kr.PromRegistry.Register(c) } return nil } // CustomMustRegister works like CustomRegister but registers any number of // StableCollectors and panics upon the first registration that causes an // error. func (kr *kubeRegistry) CustomMustRegister(cs ...StableCollector) { kr.trackStableCollectors(cs...) collectors := make([]prometheus.Collector, 0, len(cs)) for _, c := range cs { if c.Create(&kr.version, c) { kr.addResettable(c) collectors = append(collectors, c) } } kr.PromRegistry.MustRegister(collectors...) } // RawMustRegister takes a native prometheus.Collector and registers the collector // to the registry. This bypasses metrics safety checks, so should only be used // to register custom prometheus collectors. // // Deprecated func (kr *kubeRegistry) RawMustRegister(cs ...prometheus.Collector) { kr.PromRegistry.MustRegister(cs...) for _, c := range cs { kr.addResettable(c) } } // addResettable will automatically add our metric to our reset // list if it satisfies the interface func (kr *kubeRegistry) addResettable(i interface{}) { kr.resetLock.Lock() defer kr.resetLock.Unlock() if resettable, ok := i.(resettable); ok { kr.resettables = append(kr.resettables, resettable) } } // Unregister unregisters the Collector that equals the Collector passed // in as an argument. (Two Collectors are considered equal if their // Describe method yields the same set of descriptors.) The function // returns whether a Collector was unregistered. Note that an unchecked // Collector cannot be unregistered (as its Describe method does not // yield any descriptor). func (kr *kubeRegistry) Unregister(collector Collector) bool { return kr.PromRegistry.Unregister(collector) } // Gather calls the Collect method of the registered Collectors and then // gathers the collected metrics into a lexicographically sorted slice // of uniquely named MetricFamily protobufs. Gather ensures that the // returned slice is valid and self-consistent so that it can be used // for valid exposition. As an exception to the strict consistency // requirements described for metric.Desc, Gather will tolerate // different sets of label names for metrics of the same metric family. func (kr *kubeRegistry) Gather() ([]*dto.MetricFamily, error) { return kr.PromRegistry.Gather() } // trackHiddenCollector stores all hidden collectors. func (kr *kubeRegistry) trackHiddenCollector(c Registerable) { kr.hiddenCollectorsLock.Lock() defer kr.hiddenCollectorsLock.Unlock() kr.hiddenCollectors[c.FQName()] = c hiddenMetricsTotal.Inc() } // trackStableCollectors stores all custom collectors. func (kr *kubeRegistry) trackStableCollectors(cs ...StableCollector) { kr.stableCollectorsLock.Lock() defer kr.stableCollectorsLock.Unlock() kr.stableCollectors = append(kr.stableCollectors, cs...) } // enableHiddenCollectors will re-register all of the hidden collectors. func (kr *kubeRegistry) enableHiddenCollectors() { if len(kr.hiddenCollectors) == 0 { return } kr.hiddenCollectorsLock.Lock() cs := make([]Registerable, 0, len(kr.hiddenCollectors)) for _, c := range kr.hiddenCollectors { c.ClearState() cs = append(cs, c) } kr.hiddenCollectors = make(map[string]Registerable) kr.hiddenCollectorsLock.Unlock() kr.MustRegister(cs...) } // enableHiddenStableCollectors will re-register the stable collectors if there is one or more hidden metrics in it. // Since we can not register a metrics twice, so we have to unregister first then register again. func (kr *kubeRegistry) enableHiddenStableCollectors() { if len(kr.stableCollectors) == 0 { return } kr.stableCollectorsLock.Lock() cs := make([]StableCollector, 0, len(kr.stableCollectors)) for _, c := range kr.stableCollectors { if len(c.HiddenMetrics()) > 0 { kr.Unregister(c) // unregister must happens before clear state, otherwise no metrics would be unregister c.ClearState() cs = append(cs, c) } } kr.stableCollectors = nil kr.stableCollectorsLock.Unlock() kr.CustomMustRegister(cs...) } // Reset invokes Reset on all metrics that are resettable. func (kr *kubeRegistry) Reset() { kr.resetLock.RLock() defer kr.resetLock.RUnlock() for _, r := range kr.resettables { r.Reset() } } // BuildVersion is a helper function that can be easily mocked. var BuildVersion = version.Get func newKubeRegistry(v apimachineryversion.Info) *kubeRegistry { r := &kubeRegistry{ PromRegistry: prometheus.NewRegistry(), version: parseVersion(v), hiddenCollectors: make(map[string]Registerable), resettables: make([]resettable, 0), } registriesLock.Lock() defer registriesLock.Unlock() registries = append(registries, r) return r } // NewKubeRegistry creates a new vanilla Registry func NewKubeRegistry() KubeRegistry { r := newKubeRegistry(BuildVersion()) return r } func (r *kubeRegistry) RegisterMetaMetrics() { r.MustRegister(registeredMetricsTotal) r.MustRegister(disabledMetricsTotal) r.MustRegister(hiddenMetricsTotal) r.MustRegister(cardinalityEnforcementUnexpectedCategorizationsTotal) } kubernetes-component-base-1b2882b/metrics/registry_test.go000066400000000000000000000414201476422213000240310ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "strings" "sync" "testing" "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" apimachineryversion "k8s.io/apimachinery/pkg/version" ) var ( v115 = semver.MustParse("1.15.0") alphaCounter = NewCounter( &CounterOpts{ Namespace: "some_namespace", Name: "test_counter_name", Subsystem: "subsystem", StabilityLevel: ALPHA, Help: "counter help", }, ) alphaDeprecatedCounter = NewCounter( &CounterOpts{ Namespace: "some_namespace", Name: "test_alpha_dep_counter", Subsystem: "subsystem", StabilityLevel: ALPHA, Help: "counter help", DeprecatedVersion: "1.15.0", }, ) alphaHiddenCounter = NewCounter( &CounterOpts{ Namespace: "some_namespace", Name: "test_alpha_hidden_counter", Subsystem: "subsystem", StabilityLevel: ALPHA, Help: "counter help", DeprecatedVersion: "1.14.0", }, ) ) func TestShouldHide(t *testing.T) { currentVersion := parseVersion(apimachineryversion.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.1-alpha-1.12345", }) var tests = []struct { desc string deprecatedVersion string shouldHide bool }{ { desc: "current minor release should not be hidden", deprecatedVersion: "1.17.0", shouldHide: false, }, { desc: "older minor release should be hidden", deprecatedVersion: "1.16.0", shouldHide: true, }, } for _, test := range tests { tc := test t.Run(tc.desc, func(t *testing.T) { result := shouldHide(¤tVersion, parseSemver(tc.deprecatedVersion)) assert.Equalf(t, tc.shouldHide, result, "expected should hide %v, but got %v", tc.shouldHide, result) }) } } func TestRegister(t *testing.T) { var tests = []struct { desc string metrics []*Counter expectedErrors []error expectedIsCreatedValues []bool expectedIsDeprecated []bool expectedIsHidden []bool }{ { desc: "test alpha metric", metrics: []*Counter{alphaCounter}, expectedErrors: []error{nil}, expectedIsCreatedValues: []bool{true}, expectedIsDeprecated: []bool{false}, expectedIsHidden: []bool{false}, }, { desc: "test registering same metric multiple times", metrics: []*Counter{alphaCounter, alphaCounter}, expectedErrors: []error{nil, prometheus.AlreadyRegisteredError{}}, expectedIsCreatedValues: []bool{true, true}, expectedIsDeprecated: []bool{false, false}, expectedIsHidden: []bool{false, false}, }, { desc: "test alpha deprecated metric", metrics: []*Counter{alphaDeprecatedCounter}, expectedErrors: []error{nil}, expectedIsCreatedValues: []bool{true}, expectedIsDeprecated: []bool{true}, expectedIsHidden: []bool{false}, }, { desc: "test alpha hidden metric", metrics: []*Counter{alphaHiddenCounter}, expectedErrors: []error{nil}, expectedIsCreatedValues: []bool{false}, expectedIsDeprecated: []bool{true}, expectedIsHidden: []bool{true}, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) for i, m := range test.metrics { err := registry.Register(m) if err != nil && err.Error() != test.expectedErrors[i].Error() { t.Errorf("Got unexpected error %v, wanted %v", err, test.expectedErrors[i]) } if m.IsCreated() != test.expectedIsCreatedValues[i] { t.Errorf("Got isCreated == %v, wanted isCreated to be %v", m.IsCreated(), test.expectedIsCreatedValues[i]) } if m.IsDeprecated() != test.expectedIsDeprecated[i] { t.Errorf("Got IsDeprecated == %v, wanted IsDeprecated to be %v", m.IsDeprecated(), test.expectedIsDeprecated[i]) } if m.IsHidden() != test.expectedIsHidden[i] { t.Errorf("Got IsHidden == %v, wanted IsHidden to be %v", m.IsHidden(), test.expectedIsDeprecated[i]) } } }) } } func TestMustRegister(t *testing.T) { var tests = []struct { desc string metrics []*Counter registryVersion *semver.Version expectedPanics []bool }{ { desc: "test alpha metric", metrics: []*Counter{alphaCounter}, registryVersion: &v115, expectedPanics: []bool{false}, }, { desc: "test registering same metric multiple times", metrics: []*Counter{alphaCounter, alphaCounter}, registryVersion: &v115, expectedPanics: []bool{false, true}, }, { desc: "test alpha deprecated metric", metrics: []*Counter{alphaDeprecatedCounter}, registryVersion: &v115, expectedPanics: []bool{false}, }, { desc: "test must registering same deprecated metric", metrics: []*Counter{alphaDeprecatedCounter, alphaDeprecatedCounter}, registryVersion: &v115, expectedPanics: []bool{false, true}, }, { desc: "test alpha hidden metric", metrics: []*Counter{alphaHiddenCounter}, registryVersion: &v115, expectedPanics: []bool{false}, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) for i, m := range test.metrics { if test.expectedPanics[i] { assert.Panics(t, func() { registry.MustRegister(m) }, "Did not panic even though we expected it.") } else { registry.MustRegister(m) } } }) } } func TestShowHiddenMetric(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) expectedMetricCount := 0 registry.MustRegister(alphaHiddenCounter) ms, err := registry.Gather() require.NoError(t, err, "Gather failed %v", err) assert.Lenf(t, ms, expectedMetricCount, "Got %v metrics, Want: %v metrics", len(ms), expectedMetricCount) showHidden.Store(true) defer showHidden.Store(false) registry.MustRegister(NewCounter( &CounterOpts{ Namespace: "some_namespace", Name: "test_alpha_show_hidden_counter", Subsystem: "subsystem", StabilityLevel: ALPHA, Help: "counter help", DeprecatedVersion: "1.14.0", }, )) expectedMetricCount = 1 ms, err = registry.Gather() require.NoError(t, err, "Gather failed %v", err) assert.Lenf(t, ms, expectedMetricCount, "Got %v metrics, Want: %v metrics", len(ms), expectedMetricCount) } func TestValidateShowHiddenMetricsVersion(t *testing.T) { currentVersion := parseVersion(apimachineryversion.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.1-alpha-1.12345", }) var tests = []struct { desc string targetVersion string expectedError bool }{ { desc: "invalid version is not allowed", targetVersion: "1.invalid", expectedError: true, }, { desc: "patch version is not allowed", targetVersion: "1.16.0", expectedError: true, }, { desc: "old version is not allowed", targetVersion: "1.15", expectedError: true, }, { desc: "new version is not allowed", targetVersion: "1.17", expectedError: true, }, { desc: "valid version is allowed", targetVersion: "1.16", expectedError: false, }, } for _, test := range tests { tc := test t.Run(tc.desc, func(t *testing.T) { err := validateShowHiddenMetricsVersion(currentVersion, tc.targetVersion) if tc.expectedError { assert.Errorf(t, err, "Failed to test: %s", tc.desc) } else { assert.NoErrorf(t, err, "Failed to test: %s", tc.desc) } }) } } func TestEnableHiddenMetrics(t *testing.T) { currentVersion := apimachineryversion.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.1-alpha-1.12345", } var tests = []struct { name string fqName string counter *Counter mustRegister bool expectedMetric string }{ { name: "hide by register", fqName: "hidden_metric_register", counter: NewCounter(&CounterOpts{ Name: "hidden_metric_register", Help: "counter help", StabilityLevel: STABLE, DeprecatedVersion: "1.16.0", }), mustRegister: false, expectedMetric: ` # HELP hidden_metric_register [STABLE] (Deprecated since 1.16.0) counter help # TYPE hidden_metric_register counter hidden_metric_register 1 `, }, { name: "hide by must register", fqName: "hidden_metric_must_register", counter: NewCounter(&CounterOpts{ Name: "hidden_metric_must_register", Help: "counter help", StabilityLevel: STABLE, DeprecatedVersion: "1.16.0", }), mustRegister: true, expectedMetric: ` # HELP hidden_metric_must_register [STABLE] (Deprecated since 1.16.0) counter help # TYPE hidden_metric_must_register counter hidden_metric_must_register 1 `, }, } for _, test := range tests { tc := test t.Run(tc.name, func(t *testing.T) { registry := newKubeRegistry(currentVersion) if tc.mustRegister { registry.MustRegister(tc.counter) } else { _ = registry.Register(tc.counter) } tc.counter.Inc() // no-ops, because counter hasn't been initialized if err := testutil.GatherAndCompare(registry, strings.NewReader(""), tc.fqName); err != nil { t.Fatal(err) } SetShowHidden() defer func() { showHiddenOnce = *new(sync.Once) showHidden.Store(false) }() tc.counter.Inc() if err := testutil.GatherAndCompare(registry, strings.NewReader(tc.expectedMetric), tc.fqName); err != nil { t.Fatal(err) } }) } } func TestEnableHiddenStableCollector(t *testing.T) { var currentVersion = apimachineryversion.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.0-alpha-1.12345", } var normal = NewDesc("test_enable_hidden_custom_metric_normal", "this is a normal metric", []string{"name"}, nil, STABLE, "") var hiddenA = NewDesc("test_enable_hidden_custom_metric_hidden_a", "this is the hidden metric A", []string{"name"}, nil, STABLE, "1.16.0") var hiddenB = NewDesc("test_enable_hidden_custom_metric_hidden_b", "this is the hidden metric B", []string{"name"}, nil, STABLE, "1.16.0") var tests = []struct { name string descriptors []*Desc metricNames []string expectMetricsBeforeEnable string expectMetricsAfterEnable string }{ { name: "all hidden", descriptors: []*Desc{hiddenA, hiddenB}, metricNames: []string{"test_enable_hidden_custom_metric_hidden_a", "test_enable_hidden_custom_metric_hidden_b"}, expectMetricsBeforeEnable: "", expectMetricsAfterEnable: ` # HELP test_enable_hidden_custom_metric_hidden_a [STABLE] (Deprecated since 1.16.0) this is the hidden metric A # TYPE test_enable_hidden_custom_metric_hidden_a gauge test_enable_hidden_custom_metric_hidden_a{name="value"} 1 # HELP test_enable_hidden_custom_metric_hidden_b [STABLE] (Deprecated since 1.16.0) this is the hidden metric B # TYPE test_enable_hidden_custom_metric_hidden_b gauge test_enable_hidden_custom_metric_hidden_b{name="value"} 1 `, }, { name: "partial hidden", descriptors: []*Desc{normal, hiddenA, hiddenB}, metricNames: []string{"test_enable_hidden_custom_metric_normal", "test_enable_hidden_custom_metric_hidden_a", "test_enable_hidden_custom_metric_hidden_b"}, expectMetricsBeforeEnable: ` # HELP test_enable_hidden_custom_metric_normal [STABLE] this is a normal metric # TYPE test_enable_hidden_custom_metric_normal gauge test_enable_hidden_custom_metric_normal{name="value"} 1 `, expectMetricsAfterEnable: ` # HELP test_enable_hidden_custom_metric_normal [STABLE] this is a normal metric # TYPE test_enable_hidden_custom_metric_normal gauge test_enable_hidden_custom_metric_normal{name="value"} 1 # HELP test_enable_hidden_custom_metric_hidden_a [STABLE] (Deprecated since 1.16.0) this is the hidden metric A # TYPE test_enable_hidden_custom_metric_hidden_a gauge test_enable_hidden_custom_metric_hidden_a{name="value"} 1 # HELP test_enable_hidden_custom_metric_hidden_b [STABLE] (Deprecated since 1.16.0) this is the hidden metric B # TYPE test_enable_hidden_custom_metric_hidden_b gauge test_enable_hidden_custom_metric_hidden_b{name="value"} 1 `, }, } for _, test := range tests { tc := test t.Run(tc.name, func(t *testing.T) { registry := newKubeRegistry(currentVersion) customCollector := newTestCustomCollector(tc.descriptors...) registry.CustomMustRegister(customCollector) if err := testutil.GatherAndCompare(registry, strings.NewReader(tc.expectMetricsBeforeEnable), tc.metricNames...); err != nil { t.Fatalf("before enable test failed: %v", err) } SetShowHidden() defer func() { showHiddenOnce = *new(sync.Once) showHidden.Store(false) }() if err := testutil.GatherAndCompare(registry, strings.NewReader(tc.expectMetricsAfterEnable), tc.metricNames...); err != nil { t.Fatalf("after enable test failed: %v", err) } // refresh descriptors so as to share with cases. for _, d := range tc.descriptors { d.ClearState() } }) } } func TestRegistryReset(t *testing.T) { currentVersion := apimachineryversion.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.1-alpha-1.12345", } registry := newKubeRegistry(currentVersion) resettableMetric := NewCounterVec(&CounterOpts{ Name: "reset_metric", Help: "this metric can be reset", }, []string{"label"}) // gauges cannot be reset nonResettableMetric := NewGauge(&GaugeOpts{ Name: "not_reset_metric", Help: "this metric cannot be reset", }) registry.MustRegister(resettableMetric) registry.MustRegister(nonResettableMetric) resettableMetric.WithLabelValues("one").Inc() resettableMetric.WithLabelValues("two").Inc() resettableMetric.WithLabelValues("two").Inc() nonResettableMetric.Inc() nonResettableOutput := ` # HELP not_reset_metric [ALPHA] this metric cannot be reset # TYPE not_reset_metric gauge not_reset_metric 1 ` resettableOutput := ` # HELP reset_metric [ALPHA] this metric can be reset # TYPE reset_metric counter reset_metric{label="one"} 1 reset_metric{label="two"} 2 ` if err := testutil.GatherAndCompare(registry, strings.NewReader(nonResettableOutput+resettableOutput), "reset_metric", "not_reset_metric"); err != nil { t.Fatal(err) } registry.Reset() if err := testutil.GatherAndCompare(registry, strings.NewReader(nonResettableOutput), "reset_metric", "not_reset_metric"); err != nil { t.Fatal(err) } } func TestDisabledMetrics(t *testing.T) { o := NewOptions() o.DisabledMetrics = []string{"should_be_disabled"} o.Apply() currentVersion := apimachineryversion.Info{ Major: "1", Minor: "17", GitVersion: "v1.17.1-alpha-1.12345", } registry := newKubeRegistry(currentVersion) disabledMetric := NewCounterVec(&CounterOpts{ Name: "should_be_disabled", Help: "this metric should be disabled", }, []string{"label"}) // gauges cannot be reset enabledMetric := NewGauge(&GaugeOpts{ Name: "should_be_enabled", Help: "this metric should not be disabled", }) registry.MustRegister(disabledMetric) registry.MustRegister(enabledMetric) disabledMetric.WithLabelValues("one").Inc() disabledMetric.WithLabelValues("two").Inc() disabledMetric.WithLabelValues("two").Inc() enabledMetric.Inc() enabledMetricOutput := ` # HELP should_be_enabled [ALPHA] this metric should not be disabled # TYPE should_be_enabled gauge should_be_enabled 1 ` if err := testutil.GatherAndCompare(registry, strings.NewReader(enabledMetricOutput), "should_be_disabled", "should_be_enabled"); err != nil { t.Fatal(err) } } kubernetes-component-base-1b2882b/metrics/summary.go000066400000000000000000000201031476422213000226120ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "context" "sync" "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" ) const ( DefAgeBuckets = prometheus.DefAgeBuckets DefBufCap = prometheus.DefBufCap DefMaxAge = prometheus.DefMaxAge ) // Summary is our internal representation for our wrapping struct around prometheus // summaries. Summary implements both kubeCollector and ObserverMetric // // DEPRECATED: as per the metrics overhaul KEP type Summary struct { ObserverMetric *SummaryOpts lazyMetric selfCollector } // NewSummary returns an object which is Summary-like. However, nothing // will be measured until the summary is registered somewhere. // // DEPRECATED: as per the metrics overhaul KEP func NewSummary(opts *SummaryOpts) *Summary { opts.StabilityLevel.setDefaults() s := &Summary{ SummaryOpts: opts, lazyMetric: lazyMetric{stabilityLevel: opts.StabilityLevel}, } s.setPrometheusSummary(noopMetric{}) s.lazyInit(s, BuildFQName(opts.Namespace, opts.Subsystem, opts.Name)) return s } // setPrometheusSummary sets the underlying KubeGauge object, i.e. the thing that does the measurement. func (s *Summary) setPrometheusSummary(summary prometheus.Summary) { s.ObserverMetric = summary s.initSelfCollection(summary) } // DeprecatedVersion returns a pointer to the Version or nil func (s *Summary) DeprecatedVersion() *semver.Version { return parseSemver(s.SummaryOpts.DeprecatedVersion) } // initializeMetric invokes the actual prometheus.Summary object instantiation // and stores a reference to it func (s *Summary) initializeMetric() { s.SummaryOpts.annotateStabilityLevel() // this actually creates the underlying prometheus gauge. s.setPrometheusSummary(prometheus.NewSummary(s.SummaryOpts.toPromSummaryOpts())) } // initializeDeprecatedMetric invokes the actual prometheus.Summary object instantiation // but modifies the Help description prior to object instantiation. func (s *Summary) initializeDeprecatedMetric() { s.SummaryOpts.markDeprecated() s.initializeMetric() } // WithContext allows the normal Summary metric to pass in context. The context is no-op now. func (s *Summary) WithContext(ctx context.Context) ObserverMetric { return s.ObserverMetric } // SummaryVec is the internal representation of our wrapping struct around prometheus // summaryVecs. // // DEPRECATED: as per the metrics overhaul KEP type SummaryVec struct { *prometheus.SummaryVec *SummaryOpts lazyMetric originalLabels []string } // NewSummaryVec returns an object which satisfies kubeCollector and wraps the // prometheus.SummaryVec object. However, the object returned will not measure // anything unless the collector is first registered, since the metric is lazily instantiated, // and only members extracted after // registration will actually measure anything. // // DEPRECATED: as per the metrics overhaul KEP func NewSummaryVec(opts *SummaryOpts, labels []string) *SummaryVec { opts.StabilityLevel.setDefaults() fqName := BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) v := &SummaryVec{ SummaryOpts: opts, originalLabels: labels, lazyMetric: lazyMetric{stabilityLevel: opts.StabilityLevel}, } v.lazyInit(v, fqName) return v } // DeprecatedVersion returns a pointer to the Version or nil func (v *SummaryVec) DeprecatedVersion() *semver.Version { return parseSemver(v.SummaryOpts.DeprecatedVersion) } func (v *SummaryVec) initializeMetric() { v.SummaryOpts.annotateStabilityLevel() v.SummaryVec = prometheus.NewSummaryVec(v.SummaryOpts.toPromSummaryOpts(), v.originalLabels) } func (v *SummaryVec) initializeDeprecatedMetric() { v.SummaryOpts.markDeprecated() v.initializeMetric() } // Default Prometheus Vec behavior is that member extraction results in creation of a new element // if one with the unique label values is not found in the underlying stored metricMap. // This means that if this function is called but the underlying metric is not registered // (which means it will never be exposed externally nor consumed), the metric will exist in memory // for perpetuity (i.e. throughout application lifecycle). // // For reference: https://github.com/prometheus/client_golang/blob/v0.9.2/prometheus/histogram.go#L460-L470 // // In contrast, the Vec behavior in this package is that member extraction before registration // returns a permanent noop object. // WithLabelValues returns the ObserverMetric for the given slice of label // values (same order as the VariableLabels in Desc). If that combination of // label values is accessed for the first time, a new ObserverMetric is created IFF the summaryVec // has been registered to a metrics registry. func (v *SummaryVec) WithLabelValues(lvs ...string) ObserverMetric { if !v.IsCreated() { return noop } if v.LabelValueAllowLists != nil { v.LabelValueAllowLists.ConstrainToAllowedList(v.originalLabels, lvs) } else { v.initializeLabelAllowListsOnce.Do(func() { allowListLock.RLock() if allowList, ok := labelValueAllowLists[v.FQName()]; ok { v.LabelValueAllowLists = allowList allowList.ConstrainToAllowedList(v.originalLabels, lvs) } allowListLock.RUnlock() }) } return v.SummaryVec.WithLabelValues(lvs...) } // With returns the ObserverMetric for the given Labels map (the label names // must match those of the VariableLabels in Desc). If that label map is // accessed for the first time, a new ObserverMetric is created IFF the summaryVec has // been registered to a metrics registry. func (v *SummaryVec) With(labels map[string]string) ObserverMetric { if !v.IsCreated() { return noop } if v.LabelValueAllowLists != nil { v.LabelValueAllowLists.ConstrainLabelMap(labels) } else { v.initializeLabelAllowListsOnce.Do(func() { allowListLock.RLock() if allowList, ok := labelValueAllowLists[v.FQName()]; ok { v.LabelValueAllowLists = allowList allowList.ConstrainLabelMap(labels) } allowListLock.RUnlock() }) } return v.SummaryVec.With(labels) } // Delete deletes the metric where the variable labels are the same as those // passed in as labels. It returns true if a metric was deleted. // // It is not an error if the number and names of the Labels are inconsistent // with those of the VariableLabels in Desc. However, such inconsistent Labels // can never match an actual metric, so the method will always return false in // that case. func (v *SummaryVec) Delete(labels map[string]string) bool { if !v.IsCreated() { return false // since we haven't created the metric, we haven't deleted a metric with the passed in values } return v.SummaryVec.Delete(labels) } // Reset deletes all metrics in this vector. func (v *SummaryVec) Reset() { if !v.IsCreated() { return } v.SummaryVec.Reset() } // ResetLabelAllowLists resets the label allow list for the SummaryVec. // NOTE: This should only be used in test. func (v *SummaryVec) ResetLabelAllowLists() { v.initializeLabelAllowListsOnce = sync.Once{} v.LabelValueAllowLists = nil } // WithContext returns wrapped SummaryVec with context func (v *SummaryVec) WithContext(ctx context.Context) *SummaryVecWithContext { return &SummaryVecWithContext{ ctx: ctx, SummaryVec: v, } } // SummaryVecWithContext is the wrapper of SummaryVec with context. type SummaryVecWithContext struct { *SummaryVec ctx context.Context } // WithLabelValues is the wrapper of SummaryVec.WithLabelValues. func (vc *SummaryVecWithContext) WithLabelValues(lvs ...string) ObserverMetric { return vc.SummaryVec.WithLabelValues(lvs...) } // With is the wrapper of SummaryVec.With. func (vc *SummaryVecWithContext) With(labels map[string]string) ObserverMetric { return vc.SummaryVec.With(labels) } kubernetes-component-base-1b2882b/metrics/summary_test.go000066400000000000000000000203461476422213000236620ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "testing" "github.com/blang/semver/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" apimachineryversion "k8s.io/apimachinery/pkg/version" ) func TestSummary(t *testing.T) { v115 := semver.MustParse("1.15.0") var tests = []struct { desc string *SummaryOpts registryVersion *semver.Version expectedMetricCount int expectedHelp string }{ { desc: "Test non deprecated", SummaryOpts: &SummaryOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "summary help message", StabilityLevel: ALPHA, }, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "[ALPHA] summary help message", }, { desc: "Test deprecated", SummaryOpts: &SummaryOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "summary help message", DeprecatedVersion: "1.15.0", StabilityLevel: ALPHA, }, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "[ALPHA] (Deprecated since 1.15.0) summary help message", }, { desc: "Test hidden", SummaryOpts: &SummaryOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "summary help message", DeprecatedVersion: "1.14.0", }, registryVersion: &v115, expectedMetricCount: 0, expectedHelp: "summary help message", }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) c := NewSummary(test.SummaryOpts) registry.MustRegister(c) ms, err := registry.Gather() assert.Lenf(t, ms, test.expectedMetricCount, "Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) require.NoError(t, err, "Gather failed %v", err) for _, metric := range ms { assert.Equalf(t, test.expectedHelp, metric.GetHelp(), "Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) } // let's increment the counter and verify that the metric still works c.Observe(1) c.Observe(2) c.Observe(3) c.Observe(1.5) expected := 4 ms, err = registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range ms { for _, m := range mf.GetMetric() { assert.Equalf(t, expected, int(m.GetSummary().GetSampleCount()), "Got %v, want %v as the sample count", m.GetHistogram().GetSampleCount(), expected) } } }) } } func TestSummaryVec(t *testing.T) { v115 := semver.MustParse("1.15.0") var tests = []struct { desc string *SummaryOpts labels []string registryVersion *semver.Version expectedMetricCount int expectedHelp string }{ { desc: "Test non deprecated", SummaryOpts: &SummaryOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "summary help message", }, labels: []string{"label_a", "label_b"}, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "[ALPHA] summary help message", }, { desc: "Test deprecated", SummaryOpts: &SummaryOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "summary help message", DeprecatedVersion: "1.15.0", }, labels: []string{"label_a", "label_b"}, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "[ALPHA] (Deprecated since 1.15.0) summary help message", }, { desc: "Test hidden", SummaryOpts: &SummaryOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "summary help message", DeprecatedVersion: "1.14.0", }, labels: []string{"label_a", "label_b"}, registryVersion: &v115, expectedMetricCount: 0, expectedHelp: "summary help message", }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) c := NewSummaryVec(test.SummaryOpts, test.labels) registry.MustRegister(c) c.WithLabelValues("1", "2").Observe(1.0) ms, err := registry.Gather() assert.Lenf(t, ms, test.expectedMetricCount, "Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) require.NoError(t, err, "Gather failed %v", err) for _, metric := range ms { assert.Equalf(t, test.expectedHelp, metric.GetHelp(), "Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) } // let's increment the counter and verify that the metric still works c.WithLabelValues("1", "3").Observe(1.0) c.WithLabelValues("2", "3").Observe(1.0) ms, err = registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range ms { assert.Lenf(t, mf.GetMetric(), 3, "Got %v metrics, wanted 2 as the count", len(mf.GetMetric())) for _, m := range mf.GetMetric() { assert.Equalf(t, uint64(1), m.GetSummary().GetSampleCount(), "Got %v metrics, wanted 1 as the summary sample count", m.GetSummary().GetSampleCount()) } } }) } } func TestSummaryWithLabelValueAllowList(t *testing.T) { labelAllowValues := map[string]string{ "namespace_subsystem_metric_allowlist_test,label_a": "allowed", } labels := []string{"label_a", "label_b"} opts := &SummaryOpts{ Namespace: "namespace", Name: "metric_allowlist_test", Subsystem: "subsystem", } var tests = []struct { desc string labelValues [][]string expectMetricValues map[string]int }{ { desc: "Test no unexpected input", labelValues: [][]string{{"allowed", "b1"}, {"allowed", "b2"}}, expectMetricValues: map[string]int{ "allowed b1": 1, "allowed b2": 1, }, }, { desc: "Test unexpected input", labelValues: [][]string{{"allowed", "b1"}, {"not_allowed", "b1"}}, expectMetricValues: map[string]int{ "allowed b1": 1, "unexpected b1": 1, }, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { labelValueAllowLists = map[string]*MetricLabelAllowList{} registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) c := NewSummaryVec(opts, labels) registry.MustRegister(c) SetLabelAllowListFromCLI(labelAllowValues) for _, lv := range test.labelValues { c.WithLabelValues(lv...).Observe(1.0) } mfs, err := registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range mfs { if *mf.Name != BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) { continue } mfMetric := mf.GetMetric() for _, m := range mfMetric { var aValue, bValue string for _, l := range m.Label { if *l.Name == "label_a" { aValue = *l.Value } if *l.Name == "label_b" { bValue = *l.Value } } labelValuePair := aValue + " " + bValue expectedValue, ok := test.expectMetricValues[labelValuePair] assert.True(t, ok, "Got unexpected label values, lable_a is %v, label_b is %v", aValue, bValue) actualValue := int(m.GetSummary().GetSampleCount()) assert.Equalf(t, expectedValue, actualValue, "Got %v, wanted %v as the count while setting label_a to %v and label b to %v", actualValue, expectedValue, aValue, bValue) } } }) } } kubernetes-component-base-1b2882b/metrics/testutil/000077500000000000000000000000001476422213000224475ustar00rootroot00000000000000kubernetes-component-base-1b2882b/metrics/testutil/metrics.go000066400000000000000000000337261476422213000244570ustar00rootroot00000000000000/* Copyright 2019 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 testutil import ( "fmt" "io" "math" "reflect" "sort" "strings" dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" "github.com/prometheus/common/model" "k8s.io/component-base/metrics" ) var ( // MetricNameLabel is label under which model.Sample stores metric name MetricNameLabel model.LabelName = model.MetricNameLabel // QuantileLabel is label under which model.Sample stores latency quantile value QuantileLabel model.LabelName = model.QuantileLabel ) // Metrics is generic metrics for other specific metrics type Metrics map[string]model.Samples // Equal returns true if all metrics are the same as the arguments. func (m *Metrics) Equal(o Metrics) bool { var leftKeySet []string var rightKeySet []string for k := range *m { leftKeySet = append(leftKeySet, k) } for k := range o { rightKeySet = append(rightKeySet, k) } if !reflect.DeepEqual(leftKeySet, rightKeySet) { return false } for _, k := range leftKeySet { if !(*m)[k].Equal(o[k]) { return false } } return true } // NewMetrics returns new metrics which are initialized. func NewMetrics() Metrics { result := make(Metrics) return result } // ParseMetrics parses Metrics from data returned from prometheus endpoint func ParseMetrics(data string, output *Metrics) error { dec := expfmt.NewDecoder(strings.NewReader(data), expfmt.NewFormat(expfmt.TypeTextPlain)) decoder := expfmt.SampleDecoder{ Dec: dec, Opts: &expfmt.DecodeOptions{}, } for { var v model.Vector if err := decoder.Decode(&v); err != nil { if err == io.EOF { // Expected loop termination condition. return nil } continue } for _, metric := range v { name := string(metric.Metric[MetricNameLabel]) (*output)[name] = append((*output)[name], metric) } } } // TextToMetricFamilies reads 'in' as the simple and flat text-based exchange // format and creates MetricFamily proto messages. It returns the MetricFamily // proto messages in a map where the metric names are the keys, along with any // error encountered. func TextToMetricFamilies(in io.Reader) (map[string]*dto.MetricFamily, error) { var textParser expfmt.TextParser return textParser.TextToMetricFamilies(in) } // PrintSample returns formatted representation of metric Sample func PrintSample(sample *model.Sample) string { buf := make([]string, 0) // Id is a VERY special label. For 'normal' container it's useless, but it's necessary // for 'system' containers (e.g. /docker-daemon, /kubelet, etc.). We know if that's the // case by checking if there's a label "kubernetes_container_name" present. It's hacky // but it works... _, normalContainer := sample.Metric["kubernetes_container_name"] for k, v := range sample.Metric { if strings.HasPrefix(string(k), "__") { continue } if string(k) == "id" && normalContainer { continue } buf = append(buf, fmt.Sprintf("%v=%v", string(k), v)) } return fmt.Sprintf("[%v] = %v", strings.Join(buf, ","), sample.Value) } // ComputeHistogramDelta computes the change in histogram metric for a selected label. // Results are stored in after samples func ComputeHistogramDelta(before, after model.Samples, label model.LabelName) { beforeSamplesMap := make(map[string]*model.Sample) for _, bSample := range before { beforeSamplesMap[makeKey(bSample.Metric[label], bSample.Metric["le"])] = bSample } for _, aSample := range after { if bSample, found := beforeSamplesMap[makeKey(aSample.Metric[label], aSample.Metric["le"])]; found { aSample.Value = aSample.Value - bSample.Value } } } func makeKey(a, b model.LabelValue) string { return string(a) + "___" + string(b) } // GetMetricValuesForLabel returns value of metric for a given dimension func GetMetricValuesForLabel(ms Metrics, metricName, label string) map[string]int64 { samples, found := ms[metricName] result := make(map[string]int64, len(samples)) if !found { return result } for _, sample := range samples { count := int64(sample.Value) dimensionName := string(sample.Metric[model.LabelName(label)]) result[dimensionName] = count } return result } // ValidateMetrics verifies if every sample of metric has all expected labels func ValidateMetrics(metrics Metrics, metricName string, expectedLabels ...string) error { samples, ok := metrics[metricName] if !ok { return fmt.Errorf("metric %q was not found in metrics", metricName) } for _, sample := range samples { for _, l := range expectedLabels { if _, ok := sample.Metric[model.LabelName(l)]; !ok { return fmt.Errorf("metric %q is missing label %q, sample: %q", metricName, l, sample.String()) } } } return nil } // Histogram wraps prometheus histogram DTO (data transfer object) type Histogram struct { *dto.Histogram } // HistogramVec wraps a slice of Histogram. // Note that each Histogram must have the same number of buckets. type HistogramVec []*Histogram // GetAggregatedSampleCount aggregates the sample count of each inner Histogram. func (vec HistogramVec) GetAggregatedSampleCount() uint64 { var count uint64 for _, hist := range vec { count += hist.GetSampleCount() } return count } // GetAggregatedSampleSum aggregates the sample sum of each inner Histogram. func (vec HistogramVec) GetAggregatedSampleSum() float64 { var sum float64 for _, hist := range vec { sum += hist.GetSampleSum() } return sum } // Quantile first aggregates inner buckets of each Histogram, and then // computes q-th quantile of a cumulative histogram. func (vec HistogramVec) Quantile(q float64) float64 { var buckets []bucket for i, hist := range vec { for j, bckt := range hist.Bucket { if i == 0 { buckets = append(buckets, bucket{ count: float64(bckt.GetCumulativeCount()), upperBound: bckt.GetUpperBound(), }) } else { buckets[j].count += float64(bckt.GetCumulativeCount()) } } } if len(buckets) == 0 || buckets[len(buckets)-1].upperBound != math.Inf(+1) { // The list of buckets in dto.Histogram doesn't include the final +Inf bucket, so we // add it here for the rest of the samples. buckets = append(buckets, bucket{ count: float64(vec.GetAggregatedSampleCount()), upperBound: math.Inf(+1), }) } return bucketQuantile(q, buckets) } // Average computes wrapped histograms' average value. func (vec HistogramVec) Average() float64 { return vec.GetAggregatedSampleSum() / float64(vec.GetAggregatedSampleCount()) } // Validate makes sure the wrapped histograms have all necessary fields set and with valid values. func (vec HistogramVec) Validate() error { bucketSize := 0 for i, hist := range vec { if err := hist.Validate(); err != nil { return err } if i == 0 { bucketSize = len(hist.GetBucket()) } else if bucketSize != len(hist.GetBucket()) { return fmt.Errorf("found different bucket size: expect %v, but got %v at index %v", bucketSize, len(hist.GetBucket()), i) } } return nil } // GetHistogramVecFromGatherer collects a metric, that matches the input labelValue map, // from a gatherer implementing k8s.io/component-base/metrics.Gatherer interface. // Used only for testing purposes where we need to gather metrics directly from a running binary (without metrics endpoint). func GetHistogramVecFromGatherer(gatherer metrics.Gatherer, metricName string, lvMap map[string]string) (HistogramVec, error) { var metricFamily *dto.MetricFamily m, err := gatherer.Gather() if err != nil { return nil, err } metricFamily = findMetricFamily(m, metricName) if metricFamily == nil { return nil, fmt.Errorf("metric %q not found", metricName) } if len(metricFamily.GetMetric()) == 0 { return nil, fmt.Errorf("metric %q is empty", metricName) } vec := make(HistogramVec, 0) for _, metric := range metricFamily.GetMetric() { if LabelsMatch(metric, lvMap) { if hist := metric.GetHistogram(); hist != nil { vec = append(vec, &Histogram{hist}) } } } return vec, nil } func uint64Ptr(u uint64) *uint64 { return &u } // Bucket of a histogram type bucket struct { upperBound float64 count float64 } func bucketQuantile(q float64, buckets []bucket) float64 { if q < 0 { return math.Inf(-1) } if q > 1 { return math.Inf(+1) } if len(buckets) < 2 { return math.NaN() } rank := q * buckets[len(buckets)-1].count b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].count >= rank }) if b == 0 { return buckets[0].upperBound * (rank / buckets[0].count) } if b == len(buckets)-1 && math.IsInf(buckets[b].upperBound, 1) { return buckets[len(buckets)-2].upperBound } // linear approximation of b-th bucket brank := rank - buckets[b-1].count bSize := buckets[b].upperBound - buckets[b-1].upperBound bCount := buckets[b].count - buckets[b-1].count return buckets[b-1].upperBound + bSize*(brank/bCount) } // Quantile computes q-th quantile of a cumulative histogram. // It's expected the histogram is valid (by calling Validate) func (hist *Histogram) Quantile(q float64) float64 { var buckets []bucket for _, bckt := range hist.Bucket { buckets = append(buckets, bucket{ count: float64(bckt.GetCumulativeCount()), upperBound: bckt.GetUpperBound(), }) } if len(buckets) == 0 || buckets[len(buckets)-1].upperBound != math.Inf(+1) { // The list of buckets in dto.Histogram doesn't include the final +Inf bucket, so we // add it here for the rest of the samples. buckets = append(buckets, bucket{ count: float64(hist.GetSampleCount()), upperBound: math.Inf(+1), }) } return bucketQuantile(q, buckets) } // Average computes histogram's average value func (hist *Histogram) Average() float64 { return hist.GetSampleSum() / float64(hist.GetSampleCount()) } // Validate makes sure the wrapped histogram has all necessary fields set and with valid values. func (hist *Histogram) Validate() error { if hist.SampleCount == nil || hist.GetSampleCount() == 0 { return fmt.Errorf("nil or empty histogram SampleCount") } if hist.SampleSum == nil || hist.GetSampleSum() == 0 { return fmt.Errorf("nil or empty histogram SampleSum") } for _, bckt := range hist.Bucket { if bckt == nil { return fmt.Errorf("empty histogram bucket") } if bckt.UpperBound == nil || bckt.GetUpperBound() < 0 { return fmt.Errorf("nil or negative histogram bucket UpperBound") } } return nil } // GetGaugeMetricValue extracts metric value from GaugeMetric func GetGaugeMetricValue(m metrics.GaugeMetric) (float64, error) { metricProto := &dto.Metric{} if err := m.Write(metricProto); err != nil { return 0, fmt.Errorf("error writing m: %v", err) } return metricProto.Gauge.GetValue(), nil } // GetCounterMetricValue extracts metric value from CounterMetric func GetCounterMetricValue(m metrics.CounterMetric) (float64, error) { metricProto := &dto.Metric{} if err := m.(metrics.Metric).Write(metricProto); err != nil { return 0, fmt.Errorf("error writing m: %v", err) } return metricProto.Counter.GetValue(), nil } // GetHistogramMetricValue extracts sum of all samples from ObserverMetric func GetHistogramMetricValue(m metrics.ObserverMetric) (float64, error) { metricProto := &dto.Metric{} if err := m.(metrics.Metric).Write(metricProto); err != nil { return 0, fmt.Errorf("error writing m: %v", err) } return metricProto.Histogram.GetSampleSum(), nil } // GetHistogramMetricCount extracts count of all samples from ObserverMetric func GetHistogramMetricCount(m metrics.ObserverMetric) (uint64, error) { metricProto := &dto.Metric{} if err := m.(metrics.Metric).Write(metricProto); err != nil { return 0, fmt.Errorf("error writing m: %v", err) } return metricProto.Histogram.GetSampleCount(), nil } // LabelsMatch returns true if metric has all expected labels otherwise false func LabelsMatch(metric *dto.Metric, labelFilter map[string]string) bool { metricLabels := map[string]string{} for _, labelPair := range metric.Label { metricLabels[labelPair.GetName()] = labelPair.GetValue() } // length comparison then match key to values in the maps if len(labelFilter) > len(metricLabels) { return false } for labelName, labelValue := range labelFilter { if value, ok := metricLabels[labelName]; !ok || value != labelValue { return false } } return true } // GetCounterVecFromGatherer collects a counter that matches the given name // from a gatherer implementing k8s.io/component-base/metrics.Gatherer interface. // It returns all counter values that had a label with a certain name in a map // that uses the label value as keys. // // Used only for testing purposes where we need to gather metrics directly from a running binary (without metrics endpoint). func GetCounterValuesFromGatherer(gatherer metrics.Gatherer, metricName string, lvMap map[string]string, labelName string) (map[string]float64, error) { m, err := gatherer.Gather() if err != nil { return nil, err } metricFamily := findMetricFamily(m, metricName) if metricFamily == nil { return nil, fmt.Errorf("metric %q not found", metricName) } if len(metricFamily.GetMetric()) == 0 { return nil, fmt.Errorf("metric %q is empty", metricName) } values := make(map[string]float64) for _, metric := range metricFamily.GetMetric() { if LabelsMatch(metric, lvMap) { if counter := metric.GetCounter(); counter != nil { for _, labelPair := range metric.Label { if labelPair.GetName() == labelName { values[labelPair.GetValue()] = counter.GetValue() } } } } } return values, nil } func findMetricFamily(metricFamilies []*dto.MetricFamily, metricName string) *dto.MetricFamily { for _, mFamily := range metricFamilies { if mFamily.GetName() == metricName { return mFamily } } return nil } kubernetes-component-base-1b2882b/metrics/testutil/metrics_test.go000066400000000000000000000507021476422213000255070ustar00rootroot00000000000000/* Copyright 2020 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 testutil import ( "fmt" "math" "reflect" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" dto "github.com/prometheus/client_model/go" "k8s.io/component-base/metrics" "k8s.io/utils/pointer" ) func samples2Histogram(samples []float64, upperBounds []float64) Histogram { histogram := dto.Histogram{ SampleCount: uint64Ptr(0), SampleSum: pointer.Float64Ptr(0.0), } for _, ub := range upperBounds { histogram.Bucket = append(histogram.Bucket, &dto.Bucket{ CumulativeCount: uint64Ptr(0), UpperBound: pointer.Float64Ptr(ub), }) } for _, sample := range samples { for i, bucket := range histogram.Bucket { if sample < *bucket.UpperBound { *histogram.Bucket[i].CumulativeCount++ } } *histogram.SampleCount++ *histogram.SampleSum += sample } return Histogram{ &histogram, } } func TestHistogramQuantile(t *testing.T) { tests := []struct { name string samples []float64 bounds []float64 q50 float64 q90 float64 q99 float64 }{ { name: "Repeating numbers", samples: []float64{0.5, 0.5, 0.5, 0.5, 1.5, 1.5, 1.5, 1.5, 3, 3, 3, 3, 6, 6, 6, 6}, bounds: []float64{1, 2, 4, 8}, q50: 2, q90: 6.4, q99: 7.84, }, { name: "Random numbers", samples: []float64{11, 67, 61, 21, 40, 36, 52, 63, 8, 3, 67, 35, 61, 1, 36, 58}, bounds: []float64{10, 20, 40, 80}, q50: 40, q90: 72, q99: 79.2, }, { name: "The last bucket is empty", samples: []float64{6, 34, 30, 10, 20, 18, 26, 31, 4, 2, 33, 17, 30, 1, 18, 29}, bounds: []float64{10, 20, 40, 80}, q50: 20, q90: 36, q99: 39.6, }, { name: "The last bucket has positive infinity upper bound", samples: []float64{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 500}, bounds: []float64{10, 20, 40, math.Inf(1)}, q50: 5.3125, q90: 9.5625, q99: 40, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { h := samples2Histogram(test.samples, test.bounds) q50 := h.Quantile(0.5) q90 := h.Quantile(0.9) q99 := h.Quantile(0.99) q999999 := h.Quantile(0.999999) if q50 != test.q50 { t.Errorf("Expected q50 to be %v, got %v instead", test.q50, q50) } if q90 != test.q90 { t.Errorf("Expected q90 to be %v, got %v instead", test.q90, q90) } if q99 != test.q99 { t.Errorf("Expected q99 to be %v, got %v instead", test.q99, q99) } lastUpperBound := test.bounds[len(test.bounds)-1] if !(q999999 < lastUpperBound) { t.Errorf("Expected q999999 to be less than %v, got %v instead", lastUpperBound, q999999) } }) } } func TestHistogramValidate(t *testing.T) { tests := []struct { name string h Histogram err error }{ { name: "nil SampleCount", h: Histogram{ &dto.Histogram{}, }, err: fmt.Errorf("nil or empty histogram SampleCount"), }, { name: "empty SampleCount", h: Histogram{ &dto.Histogram{ SampleCount: uint64Ptr(0), }, }, err: fmt.Errorf("nil or empty histogram SampleCount"), }, { name: "nil SampleSum", h: Histogram{ &dto.Histogram{ SampleCount: uint64Ptr(1), }, }, err: fmt.Errorf("nil or empty histogram SampleSum"), }, { name: "empty SampleSum", h: Histogram{ &dto.Histogram{ SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(0.0), }, }, err: fmt.Errorf("nil or empty histogram SampleSum"), }, { name: "nil bucket", h: Histogram{ &dto.Histogram{ SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(1.0), Bucket: []*dto.Bucket{ nil, }, }, }, err: fmt.Errorf("empty histogram bucket"), }, { name: "nil bucket UpperBound", h: Histogram{ &dto.Histogram{ SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(1.0), Bucket: []*dto.Bucket{ {}, }, }, }, err: fmt.Errorf("nil or negative histogram bucket UpperBound"), }, { name: "negative bucket UpperBound", h: Histogram{ &dto.Histogram{ SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(1.0), Bucket: []*dto.Bucket{ {UpperBound: pointer.Float64Ptr(-1.0)}, }, }, }, err: fmt.Errorf("nil or negative histogram bucket UpperBound"), }, { name: "valid histogram", h: samples2Histogram( []float64{0.5, 0.5, 0.5, 0.5, 1.5, 1.5, 1.5, 1.5, 3, 3, 3, 3, 6, 6, 6, 6}, []float64{1, 2, 4, 8}, ), }, } for _, test := range tests { err := test.h.Validate() if test.err != nil { if err == nil || err.Error() != test.err.Error() { t.Errorf("Expected %q error, got %q instead", test.err, err) } } else { if err != nil { t.Errorf("Expected error to be nil, got %q instead", err) } } } } func TestLabelsMatch(t *testing.T) { cases := []struct { name string metric *dto.Metric labelFilter map[string]string expectedMatch bool }{ {name: "metric labels and labelFilter have the same labels and values", metric: &dto.Metric{ Label: []*dto.LabelPair{ {Name: pointer.StringPtr("a"), Value: pointer.StringPtr("1")}, {Name: pointer.StringPtr("b"), Value: pointer.StringPtr("2")}, {Name: pointer.StringPtr("c"), Value: pointer.StringPtr("3")}, }}, labelFilter: map[string]string{ "a": "1", "b": "2", "c": "3", }, expectedMatch: true}, {name: "metric labels contain all labelFilter labels, and labelFilter is a subset of metric labels", metric: &dto.Metric{ Label: []*dto.LabelPair{ {Name: pointer.StringPtr("a"), Value: pointer.StringPtr("1")}, {Name: pointer.StringPtr("b"), Value: pointer.StringPtr("2")}, {Name: pointer.StringPtr("c"), Value: pointer.StringPtr("3")}, }}, labelFilter: map[string]string{ "a": "1", "b": "2", }, expectedMatch: true}, {name: "metric labels don't have all labelFilter labels and value", metric: &dto.Metric{ Label: []*dto.LabelPair{ {Name: pointer.StringPtr("a"), Value: pointer.StringPtr("1")}, {Name: pointer.StringPtr("b"), Value: pointer.StringPtr("2")}, }}, labelFilter: map[string]string{ "a": "1", "b": "2", "c": "3", }, expectedMatch: false}, {name: "The intersection of metric labels and labelFilter labels is empty", metric: &dto.Metric{ Label: []*dto.LabelPair{ {Name: pointer.StringPtr("aa"), Value: pointer.StringPtr("11")}, {Name: pointer.StringPtr("bb"), Value: pointer.StringPtr("22")}, {Name: pointer.StringPtr("cc"), Value: pointer.StringPtr("33")}, }}, labelFilter: map[string]string{ "a": "1", "b": "2", "c": "3", }, expectedMatch: false}, {name: "metric labels have the same labels names but different values with labelFilter labels and value", metric: &dto.Metric{ Label: []*dto.LabelPair{ {Name: pointer.StringPtr("a"), Value: pointer.StringPtr("1")}, {Name: pointer.StringPtr("b"), Value: pointer.StringPtr("2")}, {Name: pointer.StringPtr("c"), Value: pointer.StringPtr("3")}, }}, labelFilter: map[string]string{ "a": "11", "b": "2", "c": "3", }, expectedMatch: false}, {name: "metric labels contain label name but different values with labelFilter labels and value", metric: &dto.Metric{ Label: []*dto.LabelPair{ {Name: pointer.StringPtr("a"), Value: pointer.StringPtr("1")}, {Name: pointer.StringPtr("b"), Value: pointer.StringPtr("2")}, {Name: pointer.StringPtr("c"), Value: pointer.StringPtr("33")}, {Name: pointer.StringPtr("d"), Value: pointer.StringPtr("4")}, }}, labelFilter: map[string]string{ "a": "1", "b": "2", "c": "3", }, expectedMatch: false}, {name: "metric labels is empty and labelFilter is not empty", metric: &dto.Metric{ Label: []*dto.LabelPair{}}, labelFilter: map[string]string{ "a": "1", "b": "2", "c": "3", }, expectedMatch: false}, {name: "metric labels is not empty and labelFilter is empty", metric: &dto.Metric{ Label: []*dto.LabelPair{ {Name: pointer.StringPtr("a"), Value: pointer.StringPtr("1")}, {Name: pointer.StringPtr("b"), Value: pointer.StringPtr("2")}, }}, labelFilter: map[string]string{}, expectedMatch: true}, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { got := LabelsMatch(tt.metric, tt.labelFilter) if got != tt.expectedMatch { t.Errorf("Expected %v, got %v instead", tt.expectedMatch, got) } }) } } func TestHistogramVec_GetAggregatedSampleCount(t *testing.T) { tests := []struct { name string vec HistogramVec want uint64 }{ { name: "nil case", want: 0, }, { name: "zero case", vec: HistogramVec{ &Histogram{&dto.Histogram{SampleCount: uint64Ptr(0), SampleSum: pointer.Float64Ptr(0.0)}}, }, want: 0, }, { name: "standard case", vec: HistogramVec{ &Histogram{&dto.Histogram{SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(2.0)}}, &Histogram{&dto.Histogram{SampleCount: uint64Ptr(2), SampleSum: pointer.Float64Ptr(4.0)}}, &Histogram{&dto.Histogram{SampleCount: uint64Ptr(4), SampleSum: pointer.Float64Ptr(8.0)}}, }, want: 7, }, { name: "mixed case", vec: HistogramVec{ &Histogram{&dto.Histogram{SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(2.0)}}, &Histogram{&dto.Histogram{SampleCount: uint64Ptr(0), SampleSum: pointer.Float64Ptr(0.0)}}, &Histogram{&dto.Histogram{SampleCount: uint64Ptr(2), SampleSum: pointer.Float64Ptr(4.0)}}, }, want: 3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.vec.GetAggregatedSampleCount(); got != tt.want { t.Errorf("GetAggregatedSampleCount() = %v, want %v", got, tt.want) } }) } } func TestHistogramVec_GetAggregatedSampleSum(t *testing.T) { tests := []struct { name string vec HistogramVec want float64 }{ { name: "nil case", want: 0.0, }, { name: "zero case", vec: HistogramVec{ &Histogram{&dto.Histogram{SampleCount: uint64Ptr(0), SampleSum: pointer.Float64Ptr(0.0)}}, }, want: 0.0, }, { name: "standard case", vec: HistogramVec{ &Histogram{&dto.Histogram{SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(2.0)}}, &Histogram{&dto.Histogram{SampleCount: uint64Ptr(2), SampleSum: pointer.Float64Ptr(4.0)}}, &Histogram{&dto.Histogram{SampleCount: uint64Ptr(4), SampleSum: pointer.Float64Ptr(8.0)}}, }, want: 14.0, }, { name: "mixed case", vec: HistogramVec{ &Histogram{&dto.Histogram{SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(2.0)}}, &Histogram{&dto.Histogram{SampleCount: uint64Ptr(0), SampleSum: pointer.Float64Ptr(0.0)}}, &Histogram{&dto.Histogram{SampleCount: uint64Ptr(2), SampleSum: pointer.Float64Ptr(4.0)}}, }, want: 6.0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.vec.GetAggregatedSampleSum(); got != tt.want { t.Errorf("GetAggregatedSampleSum() = %v, want %v", got, tt.want) } }) } } func TestHistogramVec_Quantile(t *testing.T) { tests := []struct { name string samples [][]float64 bounds []float64 quantile float64 want []float64 }{ { name: "duplicated histograms", samples: [][]float64{ {0.5, 0.5, 0.5, 0.5, 1.5, 1.5, 1.5, 1.5, 3, 3, 3, 3, 6, 6, 6, 6}, {0.5, 0.5, 0.5, 0.5, 1.5, 1.5, 1.5, 1.5, 3, 3, 3, 3, 6, 6, 6, 6}, {0.5, 0.5, 0.5, 0.5, 1.5, 1.5, 1.5, 1.5, 3, 3, 3, 3, 6, 6, 6, 6}, }, bounds: []float64{1, 2, 4, 8}, want: []float64{2, 6.4, 7.2, 7.84}, }, { name: "random numbers", samples: [][]float64{ {8, 35, 47, 61, 56, 69, 66, 74, 35, 69, 5, 38, 58, 40, 36, 12}, {79, 44, 57, 46, 11, 8, 53, 77, 13, 35, 38, 47, 73, 16, 26, 29}, {51, 76, 22, 55, 20, 63, 59, 66, 34, 58, 64, 16, 79, 7, 58, 28}, }, bounds: []float64{10, 20, 40, 80}, want: []float64{44.44, 72.89, 76.44, 79.29}, }, { name: "single histogram", samples: [][]float64{ {6, 34, 30, 10, 20, 18, 26, 31, 4, 2, 33, 17, 30, 1, 18, 29}, }, bounds: []float64{10, 20, 40, 80}, want: []float64{20, 36, 38, 39.6}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var vec HistogramVec for _, sample := range tt.samples { histogram := samples2Histogram(sample, tt.bounds) vec = append(vec, &histogram) } var got []float64 for _, q := range []float64{0.5, 0.9, 0.95, 0.99} { got = append(got, math.Round(vec.Quantile(q)*100)/100) } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Quantile() = %v, want %v", got, tt.want) } }) } } func TestHistogramVec_Validate(t *testing.T) { tests := []struct { name string vec HistogramVec want error }{ { name: "nil SampleCount", vec: HistogramVec{ &Histogram{&dto.Histogram{SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(1.0)}}, &Histogram{&dto.Histogram{SampleSum: pointer.Float64Ptr(2.0)}}, }, want: fmt.Errorf("nil or empty histogram SampleCount"), }, { name: "valid HistogramVec", vec: HistogramVec{ &Histogram{&dto.Histogram{SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(1.0)}}, &Histogram{&dto.Histogram{SampleCount: uint64Ptr(2), SampleSum: pointer.Float64Ptr(2.0)}}, }, }, { name: "different bucket size", vec: HistogramVec{ &Histogram{&dto.Histogram{ SampleCount: uint64Ptr(4), SampleSum: pointer.Float64Ptr(10.0), Bucket: []*dto.Bucket{ {CumulativeCount: uint64Ptr(1), UpperBound: pointer.Float64Ptr(1)}, {CumulativeCount: uint64Ptr(2), UpperBound: pointer.Float64Ptr(2)}, {CumulativeCount: uint64Ptr(5), UpperBound: pointer.Float64Ptr(4)}, }, }}, &Histogram{&dto.Histogram{ SampleCount: uint64Ptr(3), SampleSum: pointer.Float64Ptr(8.0), Bucket: []*dto.Bucket{ {CumulativeCount: uint64Ptr(1), UpperBound: pointer.Float64Ptr(2)}, {CumulativeCount: uint64Ptr(3), UpperBound: pointer.Float64Ptr(4)}, }, }}, }, want: fmt.Errorf("found different bucket size: expect 3, but got 2 at index 1"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.vec.Validate(); fmt.Sprintf("%v", got) != fmt.Sprintf("%v", tt.want) { t.Errorf("Validate() = %v, want %v", got, tt.want) } }) } } func TestGetHistogramVecFromGatherer(t *testing.T) { tests := []struct { name string lvMap map[string]string wantVec HistogramVec }{ { name: "filter with one label", lvMap: map[string]string{"label1": "value1-0"}, wantVec: HistogramVec{ &Histogram{&dto.Histogram{ SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(1.5), Bucket: []*dto.Bucket{ {CumulativeCount: uint64Ptr(0), UpperBound: pointer.Float64Ptr(0.5)}, {CumulativeCount: uint64Ptr(1), UpperBound: pointer.Float64Ptr(2.0)}, {CumulativeCount: uint64Ptr(1), UpperBound: pointer.Float64Ptr(5.0)}, }, }}, &Histogram{&dto.Histogram{ SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(2.5), Bucket: []*dto.Bucket{ {CumulativeCount: uint64Ptr(0), UpperBound: pointer.Float64Ptr(0.5)}, {CumulativeCount: uint64Ptr(0), UpperBound: pointer.Float64Ptr(2.0)}, {CumulativeCount: uint64Ptr(1), UpperBound: pointer.Float64Ptr(5.0)}, }, }}, }, }, { name: "filter with two labels", lvMap: map[string]string{"label1": "value1-0", "label2": "value2-1"}, wantVec: HistogramVec{ &Histogram{&dto.Histogram{ SampleCount: uint64Ptr(1), SampleSum: pointer.Float64Ptr(2.5), Bucket: []*dto.Bucket{ {CumulativeCount: uint64Ptr(0), UpperBound: pointer.Float64Ptr(0.5)}, {CumulativeCount: uint64Ptr(0), UpperBound: pointer.Float64Ptr(2.0)}, {CumulativeCount: uint64Ptr(1), UpperBound: pointer.Float64Ptr(5.0)}, }, }}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buckets := []float64{.5, 2, 5} // HistogramVec has two labels defined. labels := []string{"label1", "label2"} HistogramOpts := &metrics.HistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", Buckets: buckets, } vec := metrics.NewHistogramVec(HistogramOpts, labels) // Use local registry var registry = metrics.NewKubeRegistry() var gather metrics.Gatherer = registry registry.MustRegister(vec) // Observe two metrics with same value for label1 but different value of label2. vec.WithLabelValues("value1-0", "value2-0").Observe(1.5) vec.WithLabelValues("value1-0", "value2-1").Observe(2.5) vec.WithLabelValues("value1-1", "value2-0").Observe(3.5) vec.WithLabelValues("value1-1", "value2-1").Observe(4.5) metricName := fmt.Sprintf("%s_%s_%s", HistogramOpts.Namespace, HistogramOpts.Subsystem, HistogramOpts.Name) histogramVec, _ := GetHistogramVecFromGatherer(gather, metricName, tt.lvMap) if diff := cmp.Diff(tt.wantVec, histogramVec, cmpopts.IgnoreFields(dto.Histogram{}, "state", "sizeCache", "unknownFields", "CreatedTimestamp"), cmpopts.IgnoreFields(dto.Bucket{}, "state", "sizeCache", "unknownFields")); diff != "" { t.Errorf("Got unexpected HistogramVec (-want +got):\n%s", diff) } }) } } func TestGetCounterValuesFromGatherer(t *testing.T) { namespace := "namespace" subsystem := "subsystem" name := "metric_test_name" metricName := fmt.Sprintf("%s_%s_%s", namespace, subsystem, name) tests := map[string]struct { metricName string // Empty is replaced with valid name. lvMap map[string]string labelName string wantCounterValues map[string]float64 wantErr string }{ "wrong-metric": { metricName: "no-such-metric", wantErr: `metric "no-such-metric" not found`, }, "none": { metricName: metricName, lvMap: map[string]string{"no-such-label": "a"}, wantCounterValues: map[string]float64{}, }, "value1-0": { metricName: metricName, lvMap: map[string]string{"label1": "value1-0"}, labelName: "label2", wantCounterValues: map[string]float64{"value2-0": 1.5, "value2-1": 2.5}, }, "value1-1": { metricName: metricName, lvMap: map[string]string{"label1": "value1-1"}, labelName: "label2", wantCounterValues: map[string]float64{"value2-0": 3.5, "value2-1": 4.5}, }, "value1-1-value2-0-none": { metricName: metricName, lvMap: map[string]string{"label1": "value1-1", "label2": "value2-0"}, labelName: "none", wantCounterValues: map[string]float64{}, }, "value1-0-value2-0-one": { metricName: metricName, lvMap: map[string]string{"label1": "value1-0", "label2": "value2-0"}, labelName: "label2", wantCounterValues: map[string]float64{"value2-0": 1.5}, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { // CounterVec has two labels defined. labels := []string{"label1", "label2"} counterOpts := &metrics.CounterOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "counter help message", } vec := metrics.NewCounterVec(counterOpts, labels) // Use local registry var registry = metrics.NewKubeRegistry() var gather metrics.Gatherer = registry registry.MustRegister(vec) // Observe two metrics with same value for label1 but different value of label2. vec.WithLabelValues("value1-0", "value2-0").Add(1.5) vec.WithLabelValues("value1-0", "value2-1").Add(2.5) vec.WithLabelValues("value1-1", "value2-0").Add(3.5) vec.WithLabelValues("value1-1", "value2-1").Add(4.5) // The check for empty metric apparently cannot be tested: registering // a NewCounterVec with no values has the affect that it doesn't get // returned, leading to "not found". counterValues, err := GetCounterValuesFromGatherer(gather, tt.metricName, tt.lvMap, tt.labelName) if err != nil { if tt.wantErr != "" && !strings.Contains(err.Error(), tt.wantErr) { t.Errorf("expected error %q, got instead: %v", tt.wantErr, err) } return } if tt.wantErr != "" { t.Fatalf("expected error %q, got none", tt.wantErr) } if diff := cmp.Diff(tt.wantCounterValues, counterValues); diff != "" { t.Errorf("Got unexpected HistogramVec (-want +got):\n%s", diff) } }) } } kubernetes-component-base-1b2882b/metrics/testutil/promlint.go000066400000000000000000000104661476422213000246510ustar00rootroot00000000000000/* Copyright 2020 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 testutil import ( "fmt" "io" "strings" "github.com/prometheus/client_golang/prometheus/testutil/promlint" ) // exceptionMetrics is an exception list of metrics which violates promlint rules. // // The original entries come from the existing metrics when we introduce promlint. // We setup this list for allow and not fail on the current violations. // Generally speaking, you need to fix the problem for a new metric rather than add it into the list. var exceptionMetrics = []string{ // k8s.io/apiserver/pkg/server/egressselector "apiserver_egress_dialer_dial_failure_count", // counter metrics should have "_total" suffix // k8s.io/apiserver/pkg/server/healthz "apiserver_request_total", // label names should be written in 'snake_case' not 'camelCase' // k8s.io/apiserver/pkg/endpoints/filters "authenticated_user_requests", // counter metrics should have "_total" suffix "authentication_attempts", // counter metrics should have "_total" suffix // kube-apiserver "aggregator_openapi_v2_regeneration_count", "apiserver_admission_step_admission_duration_seconds_summary", "apiserver_current_inflight_requests", "apiserver_longrunning_gauge", "get_token_count", "get_token_fail_count", "ssh_tunnel_open_count", "ssh_tunnel_open_fail_count", // kube-controller-manager "attachdetach_controller_forced_detaches", "authenticated_user_requests", "authentication_attempts", "get_token_count", "get_token_fail_count", "node_collector_evictions_number", } // A Problem is an issue detected by a Linter. type Problem promlint.Problem func (p *Problem) String() string { return fmt.Sprintf("%s:%s", p.Metric, p.Text) } // A Linter is a Prometheus metrics linter. It identifies issues with metric // names, types, and metadata, and reports them to the caller. type Linter struct { promLinter *promlint.Linter } // Lint performs a linting pass, returning a slice of Problems indicating any // issues found in the metrics stream. The slice is sorted by metric name // and issue description. func (l *Linter) Lint() ([]Problem, error) { promProblems, err := l.promLinter.Lint() if err != nil { return nil, err } // Ignore problems those in exception list problems := make([]Problem, 0, len(promProblems)) for i := range promProblems { if !l.shouldIgnore(promProblems[i].Metric) { problems = append(problems, Problem(promProblems[i])) } } return problems, nil } // shouldIgnore returns true if metric in the exception list, otherwise returns false. func (l *Linter) shouldIgnore(metricName string) bool { for i := range exceptionMetrics { if metricName == exceptionMetrics[i] { return true } } return false } // NewPromLinter creates a new Linter that reads an input stream of Prometheus metrics. // Only the text exposition format is supported. func NewPromLinter(r io.Reader) *Linter { return &Linter{ promLinter: promlint.New(r), } } func mergeProblems(problems []Problem) string { var problemsMsg []string for index := range problems { problemsMsg = append(problemsMsg, problems[index].String()) } return strings.Join(problemsMsg, ",") } // shouldIgnore returns true if metric in the exception list, otherwise returns false. func shouldIgnore(metricName string) bool { for i := range exceptionMetrics { if metricName == exceptionMetrics[i] { return true } } return false } // getLintError will ignore the metrics in exception list and converts lint problem to error. func getLintError(problems []promlint.Problem) error { var filteredProblems []Problem for _, problem := range problems { if shouldIgnore(problem.Metric) { continue } filteredProblems = append(filteredProblems, Problem(problem)) } if len(filteredProblems) == 0 { return nil } return fmt.Errorf("lint error: %s", mergeProblems(filteredProblems)) } kubernetes-component-base-1b2882b/metrics/testutil/promlint_test.go000066400000000000000000000047521476422213000257110ustar00rootroot00000000000000/* Copyright 2020 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 testutil import ( "strings" "testing" ) func TestLinter(t *testing.T) { var tests = []struct { name string metric string expect string }{ { name: "problematic metric should be reported", metric: ` # HELP test_problematic_total [ALPHA] non-counter metrics should not have total suffix # TYPE test_problematic_total gauge test_problematic_total{some_label="some_value"} 1 `, expect: `non-counter metrics should not have "_total" suffix`, }, // Don't need to test metrics in exception list, they will be covered by e2e test. // In addition, we don't need to update this test when we remove metrics from exception list in the future. } for _, test := range tests { tc := test t.Run(tc.name, func(t *testing.T) { linter := NewPromLinter(strings.NewReader(tc.metric)) problems, err := linter.Lint() if err != nil { t.Fatalf("unexpected error: %v", err) } if len(problems) == 0 { t.Fatalf("expecte a problem but got none") } if problems[0].Text != tc.expect { t.Fatalf("expect: %s, but got: %s", tc.expect, problems[0]) } }) } } func TestMergeProblems(t *testing.T) { problemOne := Problem{ Metric: "metric_one", Text: "problem one", } problemTwo := Problem{ Metric: "metric_two", Text: "problem two", } var tests = []struct { name string problems []Problem expected string }{ { name: "no problem", problems: nil, expected: "", }, { name: "one problem", problems: []Problem{problemOne}, expected: "metric_one:problem one", }, { name: "more than one problem", problems: []Problem{problemOne, problemTwo}, expected: "metric_one:problem one,metric_two:problem two", }, } for _, test := range tests { tc := test t.Run(tc.name, func(t *testing.T) { got := mergeProblems(tc.problems) if tc.expected != got { t.Errorf("expected: %s, but got: %s", tc.expected, got) } }) } } kubernetes-component-base-1b2882b/metrics/testutil/testutil.go000066400000000000000000000120621476422213000246540ustar00rootroot00000000000000/* Copyright 2019 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 testutil import ( "fmt" "io" "github.com/prometheus/client_golang/prometheus/testutil" apimachineryversion "k8s.io/apimachinery/pkg/version" "k8s.io/component-base/metrics" "k8s.io/component-base/metrics/legacyregistry" ) type TB interface { Logf(format string, args ...any) Errorf(format string, args ...any) Fatalf(format string, args ...any) } // CollectAndCompare registers the provided Collector with a newly created // pedantic Registry. It then does the same as GatherAndCompare, gathering the // metrics from the pedantic Registry. func CollectAndCompare(c metrics.Collector, expected io.Reader, metricNames ...string) error { lintProblems, err := testutil.CollectAndLint(c, metricNames...) if err != nil { return err } if err := getLintError(lintProblems); err != nil { return err } return testutil.CollectAndCompare(c, expected, metricNames...) } // GatherAndCompare gathers all metrics from the provided Gatherer and compares // it to an expected output read from the provided Reader in the Prometheus text // exposition format. If any metricNames are provided, only metrics with those // names are compared. func GatherAndCompare(g metrics.Gatherer, expected io.Reader, metricNames ...string) error { lintProblems, err := testutil.GatherAndLint(g, metricNames...) if err != nil { return err } if err := getLintError(lintProblems); err != nil { return err } return testutil.GatherAndCompare(g, expected, metricNames...) } // CustomCollectAndCompare registers the provided StableCollector with a newly created // registry. It then does the same as GatherAndCompare, gathering the // metrics from the pedantic Registry. func CustomCollectAndCompare(c metrics.StableCollector, expected io.Reader, metricNames ...string) error { registry := metrics.NewKubeRegistry() registry.CustomMustRegister(c) return GatherAndCompare(registry, expected, metricNames...) } // ScrapeAndCompare calls a remote exporter's endpoint which is expected to return some metrics in // plain text format. Then it compares it with the results that the `expected` would return. // If the `metricNames` is not empty it would filter the comparison only to the given metric names. func ScrapeAndCompare(url string, expected io.Reader, metricNames ...string) error { return testutil.ScrapeAndCompare(url, expected, metricNames...) } // NewFakeKubeRegistry creates a fake `KubeRegistry` that takes the input version as `build in version`. // It should only be used in testing scenario especially for the deprecated metrics. // The input version format should be `major.minor.patch`, e.g. '1.18.0'. func NewFakeKubeRegistry(ver string) metrics.KubeRegistry { backup := metrics.BuildVersion defer func() { metrics.BuildVersion = backup }() metrics.BuildVersion = func() apimachineryversion.Info { return apimachineryversion.Info{ GitVersion: fmt.Sprintf("v%s-alpha+1.12345", ver), } } return metrics.NewKubeRegistry() } func AssertVectorCount(t TB, name string, labelFilter map[string]string, wantCount int) { metrics, err := legacyregistry.DefaultGatherer.Gather() if err != nil { t.Fatalf("Failed to gather metrics: %s", err) } counterSum := 0 for _, mf := range metrics { if mf.GetName() != name { continue // Ignore other metrics. } for _, metric := range mf.GetMetric() { if !LabelsMatch(metric, labelFilter) { continue } counterSum += int(metric.GetCounter().GetValue()) } } if wantCount != counterSum { t.Errorf("Wanted count %d, got %d for metric %s with labels %#+v", wantCount, counterSum, name, labelFilter) for _, mf := range metrics { if mf.GetName() == name { for _, metric := range mf.GetMetric() { t.Logf("\tnear match: %s", metric.String()) } } } } } func AssertHistogramTotalCount(t TB, name string, labelFilter map[string]string, wantCount int) { metrics, err := legacyregistry.DefaultGatherer.Gather() if err != nil { t.Fatalf("Failed to gather metrics: %s", err) } counterSum := 0 for _, mf := range metrics { if mf.GetName() != name { continue // Ignore other metrics. } for _, metric := range mf.GetMetric() { if !LabelsMatch(metric, labelFilter) { continue } counterSum += int(metric.GetHistogram().GetSampleCount()) } } if wantCount != counterSum { t.Errorf("Wanted count %d, got %d for metric %s with labels %#+v", wantCount, counterSum, name, labelFilter) for _, mf := range metrics { if mf.GetName() == name { for _, metric := range mf.GetMetric() { t.Logf("\tnear match: %s\n", metric.String()) } } } } } kubernetes-component-base-1b2882b/metrics/testutil/testutil_test.go000066400000000000000000000041151476422213000257130ustar00rootroot00000000000000/* Copyright 2020 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 testutil import ( "strings" "testing" "k8s.io/component-base/metrics" ) func TestNewFakeKubeRegistry(t *testing.T) { registryVersion := "1.18.0" counter := metrics.NewCounter( &metrics.CounterOpts{ Name: "test_normal_total", Help: "counter help", }, ) deprecatedCounter := metrics.NewCounter( &metrics.CounterOpts{ Name: "test_deprecated_total", Help: "counter help", DeprecatedVersion: "1.18.0", }, ) hiddenCounter := metrics.NewCounter( &metrics.CounterOpts{ Name: "test_hidden_counter", Help: "counter help", DeprecatedVersion: "1.17.0", }, ) var tests = []struct { name string metric *metrics.Counter expected string }{ { name: "normal", metric: counter, expected: ` # HELP test_normal_total [ALPHA] counter help # TYPE test_normal_total counter test_normal_total 0 `, }, { name: "deprecated", metric: deprecatedCounter, expected: ` # HELP test_deprecated_total [ALPHA] (Deprecated since 1.18.0) counter help # TYPE test_deprecated_total counter test_deprecated_total 0 `, }, { name: "hidden", metric: hiddenCounter, expected: ``, }, } for _, test := range tests { tc := test t.Run(tc.name, func(t *testing.T) { registry := NewFakeKubeRegistry(registryVersion) registry.MustRegister(tc.metric) if err := GatherAndCompare(registry, strings.NewReader(tc.expected), tc.metric.FQName()); err != nil { t.Fatal(err) } }) } } kubernetes-component-base-1b2882b/metrics/timing_histogram.go000066400000000000000000000234441476422213000244740ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "context" "sync" "time" "github.com/blang/semver/v4" promext "k8s.io/component-base/metrics/prometheusextension" ) // PrometheusTimingHistogram is the abstraction of the underlying histogram // that we want to promote from the wrapper. type PrometheusTimingHistogram interface { GaugeMetric } // TimingHistogram is our internal representation for our wrapping struct around // timing histograms. It implements both kubeCollector and GaugeMetric type TimingHistogram struct { PrometheusTimingHistogram *TimingHistogramOpts nowFunc func() time.Time lazyMetric selfCollector } var _ GaugeMetric = &TimingHistogram{} var _ Registerable = &TimingHistogram{} var _ kubeCollector = &TimingHistogram{} // NewTimingHistogram returns an object which is TimingHistogram-like. However, nothing // will be measured until the histogram is registered somewhere. func NewTimingHistogram(opts *TimingHistogramOpts) *TimingHistogram { return NewTestableTimingHistogram(time.Now, opts) } // NewTestableTimingHistogram adds injection of the clock func NewTestableTimingHistogram(nowFunc func() time.Time, opts *TimingHistogramOpts) *TimingHistogram { opts.StabilityLevel.setDefaults() h := &TimingHistogram{ TimingHistogramOpts: opts, nowFunc: nowFunc, lazyMetric: lazyMetric{stabilityLevel: opts.StabilityLevel}, } h.setPrometheusHistogram(noopMetric{}) h.lazyInit(h, BuildFQName(opts.Namespace, opts.Subsystem, opts.Name)) return h } // setPrometheusHistogram sets the underlying KubeGauge object, i.e. the thing that does the measurement. func (h *TimingHistogram) setPrometheusHistogram(histogram promext.TimingHistogram) { h.PrometheusTimingHistogram = histogram h.initSelfCollection(histogram) } // DeprecatedVersion returns a pointer to the Version or nil func (h *TimingHistogram) DeprecatedVersion() *semver.Version { return parseSemver(h.TimingHistogramOpts.DeprecatedVersion) } // initializeMetric invokes the actual prometheus.Histogram object instantiation // and stores a reference to it func (h *TimingHistogram) initializeMetric() { h.TimingHistogramOpts.annotateStabilityLevel() // this actually creates the underlying prometheus gauge. histogram, err := promext.NewTestableTimingHistogram(h.nowFunc, h.TimingHistogramOpts.toPromHistogramOpts()) if err != nil { panic(err) // handle as for regular histograms } h.setPrometheusHistogram(histogram) } // initializeDeprecatedMetric invokes the actual prometheus.Histogram object instantiation // but modifies the Help description prior to object instantiation. func (h *TimingHistogram) initializeDeprecatedMetric() { h.TimingHistogramOpts.markDeprecated() h.initializeMetric() } // WithContext allows the normal TimingHistogram metric to pass in context. The context is no-op now. func (h *TimingHistogram) WithContext(ctx context.Context) GaugeMetric { return h.PrometheusTimingHistogram } // TimingHistogramVec is the internal representation of our wrapping struct around prometheus // TimingHistogramVecs. type TimingHistogramVec struct { *promext.TimingHistogramVec *TimingHistogramOpts nowFunc func() time.Time lazyMetric originalLabels []string } var _ GaugeVecMetric = &TimingHistogramVec{} var _ Registerable = &TimingHistogramVec{} var _ kubeCollector = &TimingHistogramVec{} // NewTimingHistogramVec returns an object which satisfies the kubeCollector, Registerable, and GaugeVecMetric interfaces // and wraps an underlying promext.TimingHistogramVec object. Note well the way that // behavior depends on registration and whether this is hidden. func NewTimingHistogramVec(opts *TimingHistogramOpts, labels []string) *TimingHistogramVec { return NewTestableTimingHistogramVec(time.Now, opts, labels) } // NewTestableTimingHistogramVec adds injection of the clock. func NewTestableTimingHistogramVec(nowFunc func() time.Time, opts *TimingHistogramOpts, labels []string) *TimingHistogramVec { opts.StabilityLevel.setDefaults() fqName := BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) v := &TimingHistogramVec{ TimingHistogramVec: noopTimingHistogramVec, TimingHistogramOpts: opts, nowFunc: nowFunc, originalLabels: labels, lazyMetric: lazyMetric{stabilityLevel: opts.StabilityLevel}, } v.lazyInit(v, fqName) return v } // DeprecatedVersion returns a pointer to the Version or nil func (v *TimingHistogramVec) DeprecatedVersion() *semver.Version { return parseSemver(v.TimingHistogramOpts.DeprecatedVersion) } func (v *TimingHistogramVec) initializeMetric() { v.TimingHistogramOpts.annotateStabilityLevel() v.TimingHistogramVec = promext.NewTestableTimingHistogramVec(v.nowFunc, v.TimingHistogramOpts.toPromHistogramOpts(), v.originalLabels...) } func (v *TimingHistogramVec) initializeDeprecatedMetric() { v.TimingHistogramOpts.markDeprecated() v.initializeMetric() } // WithLabelValuesChecked, if called before this vector has been registered in // at least one registry, will return a noop gauge and // an error that passes ErrIsNotRegistered. // If called on a hidden vector, // will return a noop gauge and a nil error. // If called with a syntactic problem in the labels, will // return a noop gauge and an error about the labels. // If none of the above apply, this method will return // the appropriate vector member and a nil error. func (v *TimingHistogramVec) WithLabelValuesChecked(lvs ...string) (GaugeMetric, error) { if !v.IsCreated() { if v.IsHidden() { return noop, nil } return noop, errNotRegistered } if v.LabelValueAllowLists != nil { v.LabelValueAllowLists.ConstrainToAllowedList(v.originalLabels, lvs) } else { v.initializeLabelAllowListsOnce.Do(func() { allowListLock.RLock() if allowList, ok := labelValueAllowLists[v.FQName()]; ok { v.LabelValueAllowLists = allowList allowList.ConstrainToAllowedList(v.originalLabels, lvs) } allowListLock.RUnlock() }) } ops, err := v.TimingHistogramVec.GetMetricWithLabelValues(lvs...) if err != nil { return noop, err } return ops.(GaugeMetric), err } // WithLabelValues calls WithLabelValuesChecked // and handles errors as follows. // An error that passes ErrIsNotRegistered is ignored // and the noop gauge is returned; // all other errors cause a panic. func (v *TimingHistogramVec) WithLabelValues(lvs ...string) GaugeMetric { ans, err := v.WithLabelValuesChecked(lvs...) if err == nil || ErrIsNotRegistered(err) { return ans } panic(err) } // WithChecked, if called before this vector has been registered in // at least one registry, will return a noop gauge and // an error that passes ErrIsNotRegistered. // If called on a hidden vector, // will return a noop gauge and a nil error. // If called with a syntactic problem in the labels, will // return a noop gauge and an error about the labels. // If none of the above apply, this method will return // the appropriate vector member and a nil error. func (v *TimingHistogramVec) WithChecked(labels map[string]string) (GaugeMetric, error) { if !v.IsCreated() { if v.IsHidden() { return noop, nil } return noop, errNotRegistered } if v.LabelValueAllowLists != nil { v.LabelValueAllowLists.ConstrainLabelMap(labels) } else { v.initializeLabelAllowListsOnce.Do(func() { allowListLock.RLock() if allowList, ok := labelValueAllowLists[v.FQName()]; ok { v.LabelValueAllowLists = allowList allowList.ConstrainLabelMap(labels) } allowListLock.RUnlock() }) } ops, err := v.TimingHistogramVec.GetMetricWith(labels) return ops.(GaugeMetric), err } // With calls WithChecked and handles errors as follows. // An error that passes ErrIsNotRegistered is ignored // and the noop gauge is returned; // all other errors cause a panic. func (v *TimingHistogramVec) With(labels map[string]string) GaugeMetric { ans, err := v.WithChecked(labels) if err == nil || ErrIsNotRegistered(err) { return ans } panic(err) } // Delete deletes the metric where the variable labels are the same as those // passed in as labels. It returns true if a metric was deleted. // // It is not an error if the number and names of the Labels are inconsistent // with those of the VariableLabels in Desc. However, such inconsistent Labels // can never match an actual metric, so the method will always return false in // that case. func (v *TimingHistogramVec) Delete(labels map[string]string) bool { if !v.IsCreated() { return false // since we haven't created the metric, we haven't deleted a metric with the passed in values } return v.TimingHistogramVec.Delete(labels) } // Reset deletes all metrics in this vector. func (v *TimingHistogramVec) Reset() { if !v.IsCreated() { return } v.TimingHistogramVec.Reset() } // ResetLabelAllowLists resets the label allow list for the TimingHistogramVec. // NOTE: This should only be used in test. func (v *TimingHistogramVec) ResetLabelAllowLists() { v.initializeLabelAllowListsOnce = sync.Once{} v.LabelValueAllowLists = nil } // WithContext returns wrapped TimingHistogramVec with context func (v *TimingHistogramVec) InterfaceWithContext(ctx context.Context) GaugeVecMetric { return &TimingHistogramVecWithContext{ ctx: ctx, TimingHistogramVec: v, } } // TimingHistogramVecWithContext is the wrapper of TimingHistogramVec with context. // Currently the context is ignored. type TimingHistogramVecWithContext struct { *TimingHistogramVec ctx context.Context } kubernetes-component-base-1b2882b/metrics/timing_histogram_test.go000066400000000000000000000353021476422213000255270ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "testing" "time" "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" apimachineryversion "k8s.io/apimachinery/pkg/version" testclock "k8s.io/utils/clock/testing" ) func TestTimingHistogram(t *testing.T) { v115 := semver.MustParse("1.15.0") var tests = []struct { desc string *TimingHistogramOpts registryVersion *semver.Version expectedMetricCount int expectedHelp string }{ { desc: "Test non deprecated", TimingHistogramOpts: &TimingHistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", Buckets: DefBuckets, InitialValue: 13, }, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "EXPERIMENTAL: [ALPHA] histogram help message", }, { desc: "Test deprecated", TimingHistogramOpts: &TimingHistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", DeprecatedVersion: "1.15.0", Buckets: DefBuckets, InitialValue: 3, }, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "EXPERIMENTAL: [ALPHA] (Deprecated since 1.15.0) histogram help message", }, { desc: "Test hidden", TimingHistogramOpts: &TimingHistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", DeprecatedVersion: "1.14.0", Buckets: DefBuckets, InitialValue: 5, }, registryVersion: &v115, expectedMetricCount: 0, expectedHelp: "EXPERIMENTAL: histogram help message", }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) t0 := time.Now() clk := testclock.NewFakePassiveClock(t0) c := NewTestableTimingHistogram(clk.Now, test.TimingHistogramOpts) registry.MustRegister(c) metricChan := make(chan prometheus.Metric) go func() { c.Collect(metricChan) close(metricChan) }() m1 := <-metricChan gm1, ok := m1.(GaugeMetric) if !ok || gm1 != c.PrometheusTimingHistogram { t.Error("Unexpected metric", m1, c.PrometheusTimingHistogram) } m2, ok := <-metricChan if ok { t.Error("Unexpected second metric", m2) } ms, err := registry.Gather() assert.Lenf(t, ms, test.expectedMetricCount, "Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) require.NoError(t, err, "Gather failed %v", err) for _, metric := range ms { assert.Equalf(t, test.expectedHelp, metric.GetHelp(), "Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) } // let's exercise the metric and check that it still works v0 := test.TimingHistogramOpts.InitialValue dt1 := time.Nanosecond t1 := t0.Add(dt1) clk.SetTime(t1) var v1 float64 = 10 c.Set(v1) dt2 := time.Hour t2 := t1.Add(dt2) clk.SetTime(t2) var v2 float64 = 1e6 c.Add(v2 - v1) dt3 := time.Microsecond t3 := t2.Add(dt3) clk.SetTime(t3) c.Set(0) expectedCount := uint64(dt1 + dt2 + dt3) expectedSum := float64(dt1)*v0 + float64(dt2)*v1 + float64(dt3)*v2 ms, err = registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range ms { t.Logf("Considering metric family %s", mf.GetName()) for _, m := range mf.GetMetric() { assert.Equalf(t, expectedCount, m.GetHistogram().GetSampleCount(), "Got %v, want %v as the sample count of metric %s", m.GetHistogram().GetSampleCount(), expectedCount, m.String()) assert.InDeltaf(t, expectedSum, m.GetHistogram().GetSampleSum(), 0.01, "Got %v, want %v as the sample sum of metric %s", m.GetHistogram().GetSampleSum(), expectedSum, m.String()) } } }) } } func TestTimingHistogramVec(t *testing.T) { v115 := semver.MustParse("1.15.0") var tests = []struct { desc string *TimingHistogramOpts labels []string registryVersion *semver.Version expectedMetricCount int expectedHelp string }{ { desc: "Test non deprecated", TimingHistogramOpts: &TimingHistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", Buckets: DefBuckets, InitialValue: 5, }, labels: []string{"label_a", "label_b"}, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "EXPERIMENTAL: [ALPHA] histogram help message", }, { desc: "Test deprecated", TimingHistogramOpts: &TimingHistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", DeprecatedVersion: "1.15.0", Buckets: DefBuckets, InitialValue: 13, }, labels: []string{"label_a", "label_b"}, registryVersion: &v115, expectedMetricCount: 1, expectedHelp: "EXPERIMENTAL: [ALPHA] (Deprecated since 1.15.0) histogram help message", }, { desc: "Test hidden", TimingHistogramOpts: &TimingHistogramOpts{ Namespace: "namespace", Name: "metric_test_name", Subsystem: "subsystem", Help: "histogram help message", DeprecatedVersion: "1.14.0", Buckets: DefBuckets, InitialValue: 42, }, labels: []string{"label_a", "label_b"}, registryVersion: &v115, expectedMetricCount: 0, expectedHelp: "EXPERIMENTAL: histogram help message", }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) t0 := time.Now() clk := testclock.NewFakePassiveClock(t0) c := NewTestableTimingHistogramVec(clk.Now, test.TimingHistogramOpts, test.labels) registry.MustRegister(c) var v0 float64 = 3 cm1, err := c.WithLabelValuesChecked("1", "2") if err != nil { t.Error(err) } cm1.Set(v0) if test.expectedMetricCount > 0 { metricChan := make(chan prometheus.Metric, 2) c.Collect(metricChan) close(metricChan) m1 := <-metricChan if m1 != cm1.(prometheus.Metric) { t.Error("Unexpected metric", m1, cm1) } m2, ok := <-metricChan if ok { t.Error("Unexpected second metric", m2) } } ms, err := registry.Gather() assert.Lenf(t, ms, test.expectedMetricCount, "Got %v metrics, Want: %v metrics", len(ms), test.expectedMetricCount) require.NoError(t, err, "Gather failed %v", err) for _, metric := range ms { if metric.GetHelp() != test.expectedHelp { assert.Equalf(t, test.expectedHelp, metric.GetHelp(), "Got %s as help message, want %s", metric.GetHelp(), test.expectedHelp) } } // let's exercise the metric and verify it still works c.WithLabelValues("1", "3").Set(v0) c.WithLabelValues("2", "3").Set(v0) dt1 := time.Nanosecond t1 := t0.Add(dt1) clk.SetTime(t1) c.WithLabelValues("1", "2").Add(5.0) c.WithLabelValues("1", "3").Add(5.0) c.WithLabelValues("2", "3").Add(5.0) ms, err = registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range ms { t.Logf("Considering metric family %s", mf.String()) assert.Lenf(t, mf.GetMetric(), 3, "Got %v metrics, wanted 3 as the count for family %#+v", len(mf.GetMetric()), mf) for _, m := range mf.GetMetric() { expectedCount := uint64(dt1) expectedSum := float64(dt1) * v0 assert.Equalf(t, expectedCount, m.GetHistogram().GetSampleCount(), "Got %v, expected histogram sample count to equal %d for metric %s", m.GetHistogram().GetSampleCount(), expectedCount, m.String()) assert.InDeltaf(t, expectedSum, m.GetHistogram().GetSampleSum(), 0.01, "Got %v, expected histogram sample sum to equal %v for metric %s", m.GetHistogram().GetSampleSum(), expectedSum, m.String()) } } }) } } func TestTimingHistogramWithLabelValueAllowList(t *testing.T) { labelAllowValues := map[string]string{ "namespace_subsystem_metric_allowlist_test,label_a": "allowed", } labels := []string{"label_a", "label_b"} opts := &TimingHistogramOpts{ Namespace: "namespace", Name: "metric_allowlist_test", Subsystem: "subsystem", InitialValue: 7, } var tests = []struct { desc string labelValues [][]string expectMetricValues map[string]uint64 }{ { desc: "Test no unexpected input", labelValues: [][]string{{"allowed", "b1"}, {"allowed", "b2"}}, expectMetricValues: map[string]uint64{ "allowed b1": 1.0, "allowed b2": 1.0, }, }, { desc: "Test unexpected input", labelValues: [][]string{{"allowed", "b1"}, {"not_allowed", "b1"}}, expectMetricValues: map[string]uint64{ "allowed b1": 1.0, "unexpected b1": 1.0, }, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { labelValueAllowLists = map[string]*MetricLabelAllowList{} registry := newKubeRegistry(apimachineryversion.Info{ Major: "1", Minor: "15", GitVersion: "v1.15.0-alpha-1.12345", }) t0 := time.Now() clk := testclock.NewFakePassiveClock(t0) c := NewTestableTimingHistogramVec(clk.Now, opts, labels) registry.MustRegister(c) SetLabelAllowListFromCLI(labelAllowValues) var v0 float64 = 13 for _, lv := range test.labelValues { c.WithLabelValues(lv...).Set(v0) } dt1 := 3 * time.Hour t1 := t0.Add(dt1) clk.SetTime(t1) for _, lv := range test.labelValues { c.WithLabelValues(lv...).Add(1.0) } mfs, err := registry.Gather() require.NoError(t, err, "Gather failed %v", err) for _, mf := range mfs { if *mf.Name != BuildFQName(opts.Namespace, opts.Subsystem, opts.Name) { continue } mfMetric := mf.GetMetric() t.Logf("Consider metric family %s", mf.GetName()) for _, m := range mfMetric { var aValue, bValue string for _, l := range m.Label { if *l.Name == "label_a" { aValue = *l.Value } if *l.Name == "label_b" { bValue = *l.Value } } labelValuePair := aValue + " " + bValue expectedCount, ok := test.expectMetricValues[labelValuePair] assert.True(t, ok, "Got unexpected label values, lable_a is %v, label_b is %v", aValue, bValue) expectedSum := float64(dt1) * v0 * float64(expectedCount) expectedCount *= uint64(dt1) actualCount := m.GetHistogram().GetSampleCount() actualSum := m.GetHistogram().GetSampleSum() assert.Equalf(t, expectedCount, actualCount, "Got %v, wanted %v as the count while setting label_a to %v and label b to %v", actualCount, expectedCount, aValue, bValue) assert.InDeltaf(t, expectedSum, actualSum, 0.01, "Got %v, wanted %v as the sum while setting label_a to %v and label b to %v", actualSum, expectedSum, aValue, bValue) } } }) } } func BenchmarkTimingHistogram(b *testing.B) { b.StopTimer() now := time.Now() th := NewTestableTimingHistogram(func() time.Time { return now }, &TimingHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "testhist", Help: "Me", Buckets: []float64{1, 2, 4, 8, 16}, InitialValue: 3, }) registry := NewKubeRegistry() registry.MustRegister(th) var x int b.StartTimer() for i := 0; i < b.N; i++ { now = now.Add(time.Duration(31-x) * time.Microsecond) th.Set(float64(x)) x = (x + i) % 23 } } func BenchmarkTimingHistogramVecEltCached(b *testing.B) { b.StopTimer() now := time.Now() hv := NewTestableTimingHistogramVec(func() time.Time { return now }, &TimingHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "testhist", Help: "Me", Buckets: []float64{1, 2, 4, 8, 16}, InitialValue: 3, }, []string{"label1", "label2"}) registry := NewKubeRegistry() registry.MustRegister(hv) th, err := hv.WithLabelValuesChecked("v1", "v2") if err != nil { b.Error(err) } var x int b.StartTimer() for i := 0; i < b.N; i++ { now = now.Add(time.Duration(31-x) * time.Microsecond) th.Set(float64(x)) x = (x + i) % 23 } } func BenchmarkTimingHistogramVecEltFetched(b *testing.B) { b.StopTimer() now := time.Now() hv := NewTestableTimingHistogramVec(func() time.Time { return now }, &TimingHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "testhist", Help: "Me", Buckets: []float64{1, 2, 4, 8, 16}, InitialValue: 3, }, []string{"label1", "label2"}) registry := NewKubeRegistry() registry.MustRegister(hv) var x int b.StartTimer() for i := 0; i < b.N; i++ { now = now.Add(time.Duration(31-x) * time.Microsecond) hv.WithLabelValues("v1", "v2").Set(float64(x)) x = (x + i) % 60 } } func TestUnregisteredVec(t *testing.T) { hv := NewTestableTimingHistogramVec(time.Now, &TimingHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "testhist", Help: "Me", Buckets: []float64{1, 2, 4, 8, 16}, InitialValue: 3, }, []string{"label1", "label2"}) gauge, err := hv.WithLabelValuesChecked("v1", "v2") if gauge != noop { t.Errorf("Expected noop but got %#+v", gauge) } if !ErrIsNotRegistered(err) { t.Errorf("Expected errNotRegistered but got err=%v", err) } } func TestBadValues(t *testing.T) { hv := NewTestableTimingHistogramVec(time.Now, &TimingHistogramOpts{ Namespace: "testns", Subsystem: "testsubsys", Name: "testhist", Help: "Me", Buckets: []float64{1, 2, 4, 8, 16}, InitialValue: 3, }, []string{"label1", "label2"}) registry := NewKubeRegistry() registry.MustRegister(hv) gauge, err := hv.WithLabelValuesChecked("v1") if gauge != noop { t.Errorf("Expected noop but got %#+v", gauge) } if err == nil { t.Error("Expected an error but got nil") } if ErrIsNotRegistered(err) { t.Error("Expected an error other than errNotRegistered but got that one") } } kubernetes-component-base-1b2882b/metrics/value.go000066400000000000000000000041321476422213000222350ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "time" "github.com/prometheus/client_golang/prometheus" ) // ValueType is an enumeration of metric types that represent a simple value. type ValueType int // Possible values for the ValueType enum. const ( _ ValueType = iota CounterValue GaugeValue UntypedValue ) func (vt *ValueType) toPromValueType() prometheus.ValueType { return prometheus.ValueType(*vt) } // NewLazyConstMetric is a helper of MustNewConstMetric. // // Note: If the metrics described by the desc is hidden, the metrics will not be created. func NewLazyConstMetric(desc *Desc, valueType ValueType, value float64, labelValues ...string) Metric { if desc.IsHidden() { return nil } return prometheus.MustNewConstMetric(desc.toPrometheusDesc(), valueType.toPromValueType(), value, labelValues...) } // NewConstMetric is a helper of NewConstMetric. // // Note: If the metrics described by the desc is hidden, the metrics will not be created. func NewConstMetric(desc *Desc, valueType ValueType, value float64, labelValues ...string) (Metric, error) { if desc.IsHidden() { return nil, nil } return prometheus.NewConstMetric(desc.toPrometheusDesc(), valueType.toPromValueType(), value, labelValues...) } // NewLazyMetricWithTimestamp is a helper of NewMetricWithTimestamp. // // Warning: the Metric 'm' must be the one created by NewLazyConstMetric(), // otherwise, no stability guarantees would be offered. func NewLazyMetricWithTimestamp(t time.Time, m Metric) Metric { if m == nil { return nil } return prometheus.NewMetricWithTimestamp(t, m) } kubernetes-component-base-1b2882b/metrics/version.go000066400000000000000000000027001476422213000226050ustar00rootroot00000000000000/* Copyright 2019 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 metrics import "k8s.io/component-base/version" var ( buildInfo = NewGaugeVec( &GaugeOpts{ Name: "kubernetes_build_info", Help: "A metric with a constant '1' value labeled by major, minor, git version, git commit, git tree state, build date, Go version, and compiler from which Kubernetes was built, and platform on which it is running.", StabilityLevel: ALPHA, }, []string{"major", "minor", "git_version", "git_commit", "git_tree_state", "build_date", "go_version", "compiler", "platform"}, ) ) // RegisterBuildInfo registers the build and version info in a metadata metric in prometheus func RegisterBuildInfo(r KubeRegistry) { info := version.Get() r.MustRegister(buildInfo) buildInfo.WithLabelValues(info.Major, info.Minor, info.GitVersion, info.GitCommit, info.GitTreeState, info.BuildDate, info.GoVersion, info.Compiler, info.Platform).Set(1) } kubernetes-component-base-1b2882b/metrics/version_parser.go000066400000000000000000000023601476422213000241630ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "fmt" "regexp" "github.com/blang/semver/v4" apimachineryversion "k8s.io/apimachinery/pkg/version" ) const ( versionRegexpString = `^v(\d+\.\d+\.\d+)` ) var ( versionRe = regexp.MustCompile(versionRegexpString) ) func parseSemver(s string) *semver.Version { if s != "" { sv := semver.MustParse(s) return &sv } return nil } func parseVersion(ver apimachineryversion.Info) semver.Version { matches := versionRe.FindAllStringSubmatch(ver.String(), -1) if len(matches) != 1 { panic(fmt.Sprintf("version string \"%v\" doesn't match expected regular expression: \"%v\"", ver.String(), versionRe.String())) } return semver.MustParse(matches[0][1]) } kubernetes-component-base-1b2882b/metrics/version_parser_test.go000066400000000000000000000024531476422213000252250ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "testing" "github.com/stretchr/testify/assert" apimachineryversion "k8s.io/apimachinery/pkg/version" ) func TestVersionParsing(t *testing.T) { var tests = []struct { desc string versionString string expectedVersion string }{ { "v1.15.0-alpha-1.12345", "v1.15.0-alpha-1.12345", "1.15.0", }, { "Parse out defaulted string", "v0.0.0-master", "0.0.0", }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { version := apimachineryversion.Info{ GitVersion: test.versionString, } parsedV := parseVersion(version) assert.Equalf(t, test.expectedVersion, parsedV.String(), "Got %v, wanted %v", parsedV.String(), test.expectedVersion) }) } } kubernetes-component-base-1b2882b/metrics/wrappers.go000066400000000000000000000134601476422213000227700ustar00rootroot00000000000000/* Copyright 2019 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 metrics import ( "errors" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" ) // This file contains a series of interfaces which we explicitly define for // integrating with prometheus. We redefine the interfaces explicitly here // so that we can prevent breakage if methods are ever added to prometheus // variants of them. // Collector defines a subset of prometheus.Collector interface methods type Collector interface { Describe(chan<- *prometheus.Desc) Collect(chan<- prometheus.Metric) } // Metric defines a subset of prometheus.Metric interface methods type Metric interface { Desc() *prometheus.Desc Write(*dto.Metric) error } // CounterMetric is a Metric that represents a single numerical value that only ever // goes up. That implies that it cannot be used to count items whose number can // also go down, e.g. the number of currently running goroutines. Those // "counters" are represented by Gauges. // CounterMetric is an interface which defines a subset of the interface provided by prometheus.Counter type CounterMetric interface { Inc() Add(float64) } // CounterVecMetric is an interface which prometheus.CounterVec satisfies. type CounterVecMetric interface { WithLabelValues(...string) CounterMetric With(prometheus.Labels) CounterMetric } // GaugeMetric is an interface which defines a subset of the interface provided by prometheus.Gauge type GaugeMetric interface { Set(float64) Inc() Dec() Add(float64) Write(out *dto.Metric) error SetToCurrentTime() } // GaugeVecMetric is a collection of Gauges that differ only in label values. type GaugeVecMetric interface { // Default Prometheus Vec behavior is that member extraction results in creation of a new element // if one with the unique label values is not found in the underlying stored metricMap. // This means that if this function is called but the underlying metric is not registered // (which means it will never be exposed externally nor consumed), the metric would exist in memory // for perpetuity (i.e. throughout application lifecycle). // // For reference: https://github.com/prometheus/client_golang/blob/v0.9.2/prometheus/gauge.go#L190-L208 // // In contrast, the Vec behavior in this package is that member extraction before registration // returns a permanent noop object. // WithLabelValuesChecked, if called before this vector has been registered in // at least one registry, will return a noop gauge and // an error that passes ErrIsNotRegistered. // If called on a hidden vector, // will return a noop gauge and a nil error. // If called with a syntactic problem in the labels, will // return a noop gauge and an error about the labels. // If none of the above apply, this method will return // the appropriate vector member and a nil error. WithLabelValuesChecked(labelValues ...string) (GaugeMetric, error) // WithLabelValues calls WithLabelValuesChecked // and handles errors as follows. // An error that passes ErrIsNotRegistered is ignored // and the noop gauge is returned; // all other errors cause a panic. WithLabelValues(labelValues ...string) GaugeMetric // WithChecked, if called before this vector has been registered in // at least one registry, will return a noop gauge and // an error that passes ErrIsNotRegistered. // If called on a hidden vector, // will return a noop gauge and a nil error. // If called with a syntactic problem in the labels, will // return a noop gauge and an error about the labels. // If none of the above apply, this method will return // the appropriate vector member and a nil error. WithChecked(labels map[string]string) (GaugeMetric, error) // With calls WithChecked and handles errors as follows. // An error that passes ErrIsNotRegistered is ignored // and the noop gauge is returned; // all other errors cause a panic. With(labels map[string]string) GaugeMetric // Delete asserts that the vec should have no member for the given label set. // The returned bool indicates whether there was a change. // The return will certainly be `false` if the given label set has the wrong // set of label names. Delete(map[string]string) bool // Reset removes all the members Reset() } // ObserverMetric captures individual observations. type ObserverMetric interface { Observe(float64) } // PromRegistry is an interface which implements a subset of prometheus.Registerer and // prometheus.Gatherer interfaces type PromRegistry interface { Register(prometheus.Collector) error MustRegister(...prometheus.Collector) Unregister(prometheus.Collector) bool Gather() ([]*dto.MetricFamily, error) } // Gatherer is the interface for the part of a registry in charge of gathering // the collected metrics into a number of MetricFamilies. type Gatherer interface { prometheus.Gatherer } // Registerer is the interface for the part of a registry in charge of registering // the collected metrics. type Registerer interface { prometheus.Registerer } // GaugeFunc is a Gauge whose value is determined at collect time by calling a // provided function. // // To create GaugeFunc instances, use NewGaugeFunc. type GaugeFunc interface { Metric Collector } func ErrIsNotRegistered(err error) bool { return err == errNotRegistered } var errNotRegistered = errors.New("metric vec is not registered yet") kubernetes-component-base-1b2882b/term/000077500000000000000000000000001476422213000200735ustar00rootroot00000000000000kubernetes-component-base-1b2882b/term/OWNERS000066400000000000000000000004271476422213000210360ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners # Currently assigned this directory to sig-cli since the main use of # term seems to be for getting terminal size when printing help text. approvers: - sig-cli-maintainers reviewers: - sig-cli-reviewers labels: - sig/cli kubernetes-component-base-1b2882b/term/term.go000066400000000000000000000022371476422213000213750ustar00rootroot00000000000000/* Copyright 2019 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 term import ( "fmt" "io" "github.com/moby/term" ) // TerminalSize returns the current width and height of the user's terminal. If it isn't a terminal, // nil is returned. On error, zero values are returned for width and height. // Usually w must be the stdout of the process. Stderr won't work. func TerminalSize(w io.Writer) (int, int, error) { outFd, isTerminal := term.GetFdInfo(w) if !isTerminal { return 0, 0, fmt.Errorf("given writer is no terminal") } winsize, err := term.GetWinsize(outFd) if err != nil { return 0, 0, err } return int(winsize.Width), int(winsize.Height), nil } kubernetes-component-base-1b2882b/tracing/000077500000000000000000000000001476422213000205535ustar00rootroot00000000000000kubernetes-component-base-1b2882b/tracing/OWNERS000066400000000000000000000002551476422213000215150ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners approvers: - sig-instrumentation-approvers reviewers: - sig-instrumentation-reviewers labels: - sig/instrumentation kubernetes-component-base-1b2882b/tracing/api/000077500000000000000000000000001476422213000213245ustar00rootroot00000000000000kubernetes-component-base-1b2882b/tracing/api/OWNERS000066400000000000000000000002771476422213000222720ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners options: no_parent_owners: true approvers: - api-approvers reviewers: - sig-instrumentation-reviewers labels: - sig/instrumentation kubernetes-component-base-1b2882b/tracing/api/v1/000077500000000000000000000000001476422213000216525ustar00rootroot00000000000000kubernetes-component-base-1b2882b/tracing/api/v1/config.go000066400000000000000000000046261476422213000234560ustar00rootroot00000000000000/* Copyright 2021 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 v1 import ( "fmt" "net/url" "strings" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/component-base/featuregate" ) var ( maxSamplingRatePerMillion = int32(1000000) ) // ValidateTracingConfiguration validates the tracing configuration func ValidateTracingConfiguration(traceConfig *TracingConfiguration, featureGate featuregate.FeatureGate, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if traceConfig == nil { return allErrs } if traceConfig.SamplingRatePerMillion != nil { allErrs = append(allErrs, validateSamplingRate(*traceConfig.SamplingRatePerMillion, fldPath.Child("samplingRatePerMillion"))...) } if traceConfig.Endpoint != nil { allErrs = append(allErrs, validateEndpoint(*traceConfig.Endpoint, fldPath.Child("endpoint"))...) } return allErrs } func validateSamplingRate(rate int32, fldPath *field.Path) field.ErrorList { errs := field.ErrorList{} if rate < 0 { errs = append(errs, field.Invalid( fldPath, rate, "sampling rate must be positive", )) } if rate > maxSamplingRatePerMillion { errs = append(errs, field.Invalid( fldPath, rate, "sampling rate per million must be less than or equal to one million", )) } return errs } func validateEndpoint(endpoint string, fldPath *field.Path) field.ErrorList { errs := field.ErrorList{} if !strings.Contains(endpoint, "//") { endpoint = "dns://" + endpoint } url, err := url.Parse(endpoint) if err != nil { errs = append(errs, field.Invalid( fldPath, endpoint, err.Error(), )) return errs } switch url.Scheme { case "dns": case "unix": case "unix-abstract": default: errs = append(errs, field.Invalid( fldPath, endpoint, fmt.Sprintf("unsupported scheme: %v. Options are none, dns, unix, or unix-abstract. See https://github.com/grpc/grpc/blob/master/doc/naming.md", url.Scheme), )) } return errs } kubernetes-component-base-1b2882b/tracing/api/v1/config_test.go000066400000000000000000000051701476422213000245100ustar00rootroot00000000000000/* Copyright 2021 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 v1 import ( "testing" "k8s.io/apimachinery/pkg/util/validation/field" ) func TestValidateTracingConfiguration(t *testing.T) { samplingRate := int32(12378) negativeRate := int32(-1) tooHighRate := int32(1000001) validEndpoint := "localhost:4317" dnsEndpoint := "dns://google.com:4317" unixEndpoint := "unix://path/to/socket" invalidURL := "dn%2s://localhost:4317" httpEndpoint := "http://localhost:4317" testcases := []struct { name string expectError bool contents *TracingConfiguration }{ { name: "sampling-rate-valid", expectError: false, contents: &TracingConfiguration{ SamplingRatePerMillion: &samplingRate, }, }, { name: "sampling-rate-negative", expectError: true, contents: &TracingConfiguration{ SamplingRatePerMillion: &negativeRate, }, }, { name: "sampling-rate-negative", expectError: true, contents: &TracingConfiguration{ SamplingRatePerMillion: &tooHighRate, }, }, { name: "default Endpoint", expectError: false, contents: &TracingConfiguration{ Endpoint: &validEndpoint, }, }, { name: "dns Endpoint", expectError: false, contents: &TracingConfiguration{ Endpoint: &dnsEndpoint, }, }, { name: "unix Endpoint", expectError: false, contents: &TracingConfiguration{ Endpoint: &unixEndpoint, }, }, { name: "invalid Endpoint", expectError: true, contents: &TracingConfiguration{ Endpoint: &httpEndpoint, }, }, { name: "invalid url", expectError: true, contents: &TracingConfiguration{ Endpoint: &invalidURL, }, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { errs := ValidateTracingConfiguration(tc.contents, nil, field.NewPath("tracing")) if !tc.expectError && len(errs) != 0 { t.Errorf("Calling ValidateTracingConfiguration expected no error, got %v", errs) } else if tc.expectError && len(errs) == 0 { t.Errorf("Calling ValidateTracingConfiguration expected error, got no error") } }) } } kubernetes-component-base-1b2882b/tracing/api/v1/doc.go000066400000000000000000000022151476422213000227460ustar00rootroot00000000000000/* Copyright 2022 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. */ // +k8s:deepcopy-gen=package // Package v1 contains the configuration API for tracing. // // The intention is to only have a single version of this API, potentially with // new fields added over time in a backwards-compatible manner. Fields for // alpha or beta features are allowed as long as they are defined so that not // changing the defaults leaves those features disabled. // // The "v1" package name is just a reminder that API compatibility rules apply, // not an indication of the stability of all features covered by it. package v1 // import "k8s.io/component-base/tracing/api/v1" kubernetes-component-base-1b2882b/tracing/api/v1/types.go000066400000000000000000000023761476422213000233550ustar00rootroot00000000000000/* Copyright 2022 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 v1 // TracingConfiguration provides versioned configuration for OpenTelemetry tracing clients. type TracingConfiguration struct { // Endpoint of the collector this component will report traces to. // The connection is insecure, and does not currently support TLS. // Recommended is unset, and endpoint is the otlp grpc default, localhost:4317. // +optional Endpoint *string `json:"endpoint,omitempty"` // SamplingRatePerMillion is the number of samples to collect per million spans. // Recommended is unset. If unset, sampler respects its parent span's sampling // rate, but otherwise never samples. // +optional SamplingRatePerMillion *int32 `json:"samplingRatePerMillion,omitempty"` } kubernetes-component-base-1b2882b/tracing/api/v1/zz_generated.deepcopy.go000066400000000000000000000026331476422213000264750ustar00rootroot00000000000000//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 v1 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TracingConfiguration) DeepCopyInto(out *TracingConfiguration) { *out = *in if in.Endpoint != nil { in, out := &in.Endpoint, &out.Endpoint *out = new(string) **out = **in } if in.SamplingRatePerMillion != nil { in, out := &in.SamplingRatePerMillion, &out.SamplingRatePerMillion *out = new(int32) **out = **in } return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TracingConfiguration. func (in *TracingConfiguration) DeepCopy() *TracingConfiguration { if in == nil { return nil } out := new(TracingConfiguration) in.DeepCopyInto(out) return out } kubernetes-component-base-1b2882b/tracing/tracing.go000066400000000000000000000073201476422213000225330ustar00rootroot00000000000000/* Copyright 2022 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 tracing import ( "context" "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" utiltrace "k8s.io/utils/trace" ) const instrumentationScope = "k8s.io/component-base/tracing" // Start creates spans using both OpenTelemetry, and the k8s.io/utils/trace package. // It only creates an OpenTelemetry span if the incoming context already includes a span. func Start(ctx context.Context, name string, attributes ...attribute.KeyValue) (context.Context, *Span) { // If the incoming context already includes an OpenTelemetry span, create a child span with the provided name and attributes. // If the caller is not using OpenTelemetry, or has tracing disabled (e.g. with a component-specific feature flag), this is a noop. ctx, otelSpan := trace.SpanFromContext(ctx).TracerProvider().Tracer(instrumentationScope).Start(ctx, name, trace.WithAttributes(attributes...)) // If there is already a utiltrace span in the context, use that as our parent span. utilSpan := utiltrace.FromContext(ctx).Nest(name, attributesToFields(attributes)...) // Set the trace as active in the context so that subsequent Start calls create nested spans. return utiltrace.ContextWithTrace(ctx, utilSpan), &Span{ otelSpan: otelSpan, utilSpan: utilSpan, } } // Span is a component part of a trace. It represents a single named // and timed operation of a workflow being observed. // This Span is a combination of an OpenTelemetry and k8s.io/utils/trace span // to facilitate the migration to OpenTelemetry. type Span struct { otelSpan trace.Span utilSpan *utiltrace.Trace } // AddEvent adds a point-in-time event with a name and attributes. func (s *Span) AddEvent(name string, attributes ...attribute.KeyValue) { s.otelSpan.AddEvent(name, trace.WithAttributes(attributes...)) if s.utilSpan != nil { s.utilSpan.Step(name, attributesToFields(attributes)...) } } // End ends the span, and logs if the span duration is greater than the logThreshold. func (s *Span) End(logThreshold time.Duration) { s.otelSpan.End() if s.utilSpan != nil { s.utilSpan.LogIfLong(logThreshold) } } // RecordError will record err as an exception span event for this span. // If this span is not being recorded or err is nil then this method does nothing. func (s *Span) RecordError(err error, attributes ...attribute.KeyValue) { s.otelSpan.RecordError(err, trace.WithAttributes(attributes...)) } func attributesToFields(attributes []attribute.KeyValue) []utiltrace.Field { fields := make([]utiltrace.Field, len(attributes)) for i := range attributes { attr := attributes[i] fields[i] = utiltrace.Field{Key: string(attr.Key), Value: attr.Value.AsInterface()} } return fields } // SpanFromContext returns the *Span from the current context. It is composed of the active // OpenTelemetry and k8s.io/utils/trace spans. func SpanFromContext(ctx context.Context) *Span { return &Span{ otelSpan: trace.SpanFromContext(ctx), utilSpan: utiltrace.FromContext(ctx), } } // ContextWithSpan returns a context with the Span included in the context. func ContextWithSpan(ctx context.Context, s *Span) context.Context { return trace.ContextWithSpan(utiltrace.ContextWithTrace(ctx, s.utilSpan), s.otelSpan) } kubernetes-component-base-1b2882b/tracing/tracing_test.go000066400000000000000000000143201476422213000235700ustar00rootroot00000000000000/* Copyright 2022 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 tracing import ( "bytes" "context" "flag" "fmt" "strings" "testing" "time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk/trace" "go.opentelemetry.io/otel/sdk/trace/tracetest" "k8s.io/klog/v2" utiltrace "k8s.io/utils/trace" ) func init() { klog.InitFlags(flag.CommandLine) _ = flag.CommandLine.Lookup("logtostderr").Value.Set("false") _ = flag.CommandLine.Lookup("v").Value.Set("2") } func TestOpenTelemetryTracing(t *testing.T) { // Setup OpenTelemetry for testing fakeRecorder := tracetest.NewSpanRecorder() otelTracer := trace.NewTracerProvider(trace.WithSpanProcessor(fakeRecorder)).Tracer(instrumentationScope) func() { ctx := context.Background() // Create a parent OpenTelemetry span ctx, span := otelTracer.Start(ctx, "parent otel span") defer span.End() // Creates a child span _, tr := Start(ctx, "frobber", attribute.String("foo", "bar")) defer tr.End(10 * time.Second) time.Sleep(5 * time.Millisecond) // Add one event to the frobber span tr.AddEvent("reticulated splines", attribute.Bool("should I do it?", false)) // took 5ms time.Sleep(10 * time.Millisecond) // Add error event to the frobber span tr.RecordError(fmt.Errorf("something went wrong")) // Ensure setting context with span makes the next span a child ctx = ContextWithSpan(context.Background(), tr) // Add another event to the frobber span after getting the span from context SpanFromContext(ctx).AddEvent("sequenced particles", attribute.Int("inches in foot", 12)) // took 10ms // Creates a nested child span _, tr = Start(ctx, "nested child span") defer tr.End(10 * time.Second) }() output := fakeRecorder.Ended() if len(output) != 3 { t.Fatalf("got %d; expected len(output) == 3", len(output)) } // Nested child span is ended first nestedChild := output[0] if nestedChild.Name() != "nested child span" { t.Fatalf("got %s; expected nestedChild.Name() == nested child span", nestedChild.Name()) } // Child span is ended second child := output[1] if !nestedChild.Parent().Equal(child.SpanContext()) { t.Errorf("got child: %v, parent: %v; expected child.Parent() == parent.SpanContext()", nestedChild.Parent(), child.SpanContext()) } if child.Name() != "frobber" { t.Errorf("got %s; expected child.Name() == frobber", child.Name()) } if len(child.Attributes()) != 1 { t.Errorf("got attributes %v; expected one attribute in child.Attributes()", child.Attributes()) } if len(child.Events()) != 3 { t.Errorf("got events %v; expected 2 events in child.Events()", child.Events()) } if child.Events()[0].Name != "reticulated splines" { t.Errorf("got event %v; expected child.Events()[0].Name == reticulated splines", child.Events()[0]) } if len(child.Events()[0].Attributes) != 1 { t.Errorf("got event %v; expected 1 attribute in child.Events()[0].Attributes", child.Events()[0]) } if child.Events()[1].Name != "exception" { t.Errorf("got event %v; expected child.Events()[1].Name == something went wrong", child.Events()[1]) } if len(child.Events()[1].Attributes) != 2 { t.Errorf("got event %#v; expected 2 attribute in child.Events()[1].Attributes", child.Events()[1]) } if child.Events()[2].Name != "sequenced particles" { t.Errorf("got event %v; expected child.Events()[2].Name == sequenced particles", child.Events()[2]) } if len(child.Events()[2].Attributes) != 1 { t.Errorf("got event %v; expected 1 attribute in child.Events()[2].Attributes", child.Events()[2]) } // Parent span is ended last parent := output[2] if !child.Parent().Equal(parent.SpanContext()) { t.Fatalf("got child: %v, parent: %v; expected child.Parent() == parent.SpanContext()", child.Parent(), parent.SpanContext()) } if parent.Name() != "parent otel span" { t.Fatalf("got %s; expected parent.Name() == parent otel span", parent.Name()) } if len(parent.Attributes()) != 0 { t.Fatalf("got attributes %v; expected empty parent.Attributes()", parent.Attributes()) } } func TestUtilTracing(t *testing.T) { var buf bytes.Buffer klog.SetOutput(&buf) ctx := context.Background() // Create a utiltracing span tr0 := utiltrace.New("parent utiltrace span") ctx = utiltrace.ContextWithTrace(ctx, tr0) // Creates a child span _, tr1 := Start(ctx, "frobber", attribute.String("foo", "bar")) time.Sleep(5 * time.Millisecond) // Add one event to the frobber span tr1.AddEvent("reticulated splines", attribute.Bool("should I do it?", false)) // took 5ms time.Sleep(10 * time.Millisecond) // Ensure setting context with span makes the next span a child ctx = ContextWithSpan(context.Background(), tr1) // Add another event to the frobber span after getting the span from context SpanFromContext(ctx).AddEvent("sequenced particles", attribute.Int("inches in foot", 12)) // took 10ms // Creates a nested child span _, tr2 := Start(ctx, "nested child span") // always log tr2.End(0 * time.Second) tr1.End(0 * time.Second) // Since all traces are nested, no logging should have occurred yet if buf.String() != "" { t.Errorf("child traces were printed out before the parent span completed: %v", buf.String()) } // Now, end the parent span to cause logging to occur tr0.Log() expected := []string{ `"frobber" foo:bar`, `---"reticulated splines" should I do it?:false`, `---"sequenced particles" inches in foot:12`, `"nested child span"`, `"parent utiltrace span"`, } for _, msg := range expected { if !strings.Contains(buf.String(), msg) { t.Errorf("\nMsg %q not found in log: \n%v\n", msg, buf.String()) } } } func TestContextNoPanic(t *testing.T) { ctx := context.Background() // Make sure calling functions on spans from context doesn't panic SpanFromContext(ctx).AddEvent("foo") SpanFromContext(ctx).End(time.Minute) } kubernetes-component-base-1b2882b/tracing/utils.go000066400000000000000000000111651476422213000222460ustar00rootroot00000000000000/* Copyright 2021 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 tracing import ( "context" "net/http" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.17.0" oteltrace "go.opentelemetry.io/otel/trace" noopoteltrace "go.opentelemetry.io/otel/trace/noop" "k8s.io/client-go/transport" "k8s.io/component-base/tracing/api/v1" ) // TracerProvider is an OpenTelemetry TracerProvider which can be shut down type TracerProvider interface { oteltrace.TracerProvider Shutdown(context.Context) error } type noopTracerProvider struct { oteltrace.TracerProvider } func (n *noopTracerProvider) Shutdown(context.Context) error { return nil } func NewNoopTracerProvider() TracerProvider { return &noopTracerProvider{TracerProvider: noopoteltrace.NewTracerProvider()} } // NewProvider creates a TracerProvider in a component, and enforces recommended tracing behavior func NewProvider(ctx context.Context, tracingConfig *v1.TracingConfiguration, addedOpts []otlptracegrpc.Option, resourceOpts []resource.Option, ) (TracerProvider, error) { if tracingConfig == nil { return NewNoopTracerProvider(), nil } opts := append([]otlptracegrpc.Option{}, addedOpts...) if tracingConfig.Endpoint != nil { opts = append(opts, otlptracegrpc.WithEndpoint(*tracingConfig.Endpoint)) } opts = append(opts, otlptracegrpc.WithInsecure()) exporter, err := otlptracegrpc.New(ctx, opts...) if err != nil { return nil, err } res, err := resource.New(ctx, resourceOpts...) if err != nil { return nil, err } // sampler respects parent span's sampling rate or // otherwise never samples. sampler := sdktrace.NeverSample() // Or, emit spans for a fraction of transactions if tracingConfig.SamplingRatePerMillion != nil && *tracingConfig.SamplingRatePerMillion > 0 { sampler = sdktrace.TraceIDRatioBased(float64(*tracingConfig.SamplingRatePerMillion) / float64(1000000)) } // batch span processor to aggregate spans before export. bsp := sdktrace.NewBatchSpanProcessor(exporter) tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sdktrace.ParentBased(sampler)), sdktrace.WithSpanProcessor(bsp), sdktrace.WithResource(res), ) return tp, nil } // WithTracing adds tracing to requests if the incoming request is sampled func WithTracing(handler http.Handler, tp oteltrace.TracerProvider, spanName string) http.Handler { opts := []otelhttp.Option{ otelhttp.WithPropagators(Propagators()), otelhttp.WithTracerProvider(tp), } wrappedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Add the http.target attribute to the otelhttp span // Workaround for https://github.com/open-telemetry/opentelemetry-go-contrib/issues/3743 if r.URL != nil { oteltrace.SpanFromContext(r.Context()).SetAttributes(semconv.HTTPTarget(r.URL.RequestURI())) } handler.ServeHTTP(w, r) }) // With Noop TracerProvider, the otelhttp still handles context propagation. // See https://github.com/open-telemetry/opentelemetry-go/tree/main/example/passthrough return otelhttp.NewHandler(wrappedHandler, spanName, opts...) } // WrapperFor can be used to add tracing to a *rest.Config. // Example usage: // tp := NewProvider(...) // config, _ := rest.InClusterConfig() // config.Wrap(WrapperFor(tp)) // kubeclient, _ := clientset.NewForConfig(config) func WrapperFor(tp oteltrace.TracerProvider) transport.WrapperFunc { return func(rt http.RoundTripper) http.RoundTripper { opts := []otelhttp.Option{ otelhttp.WithPropagators(Propagators()), otelhttp.WithTracerProvider(tp), } // With Noop TracerProvider, the otelhttp still handles context propagation. // See https://github.com/open-telemetry/opentelemetry-go/tree/main/example/passthrough return otelhttp.NewTransport(rt, opts...) } } // Propagators returns the recommended set of propagators. func Propagators() propagation.TextMapPropagator { return propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}) } kubernetes-component-base-1b2882b/version/000077500000000000000000000000001476422213000206115ustar00rootroot00000000000000kubernetes-component-base-1b2882b/version/OWNERS000066400000000000000000000007441476422213000215560ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners # Currently assigned this directory to sig-api-machinery since this is # an interface to the version definition in "k8s.io/apimachinery/pkg/version", # and also to sig-release since this version information is set up for # each release. approvers: - sig-api-machinery-api-approvers - release-engineering-approvers reviewers: - sig-api-machinery-api-reviewers - release-managers labels: - sig/api-machinery - sig/release kubernetes-component-base-1b2882b/version/base.go000066400000000000000000000062071476422213000220570ustar00rootroot00000000000000/* Copyright 2019 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 version // Base version information. // // This is the fallback data used when version information from git is not // provided via go ldflags. It provides an approximation of the Kubernetes // version for ad-hoc builds (e.g. `go build`) that cannot get the version // information from git. // // If you are looking at these fields in the git tree, they look // strange. They are modified on the fly by the build process. The // in-tree values are dummy values used for "git archive", which also // works for GitHub tar downloads. // // When releasing a new Kubernetes version, this file is updated by // build/mark_new_version.sh to reflect the new version, and then a // git annotated tag (using format vX.Y where X == Major version and Y // == Minor version) is created to point to the commit that updates // component-base/version/base.go var ( // TODO: Deprecate gitMajor and gitMinor, use only gitVersion // instead. First step in deprecation, keep the fields but make // them irrelevant. (Next we'll take it out, which may muck with // scripts consuming the kubectl version output - but most of // these should be looking at gitVersion already anyways.) gitMajor string // major version, always numeric gitMinor string // minor version, numeric possibly followed by "+" // semantic version, derived by build scripts (see // https://github.com/kubernetes/community/blob/master/contributors/design-proposals/release/versioning.md // for a detailed discussion of this field) // // TODO: This field is still called "gitVersion" for legacy // reasons. For prerelease versions, the build metadata on the // semantic version is a git hash, but the version itself is no // longer the direct output of "git describe", but a slight // translation to be semver compliant. // NOTE: The $Format strings are replaced during 'git archive' thanks to the // companion .gitattributes file containing 'export-subst' in this same // directory. See also https://git-scm.com/docs/gitattributes gitVersion = "v0.0.0-master+$Format:%H$" gitCommit = "$Format:%H$" // sha1 from git, output of $(git rev-parse HEAD) gitTreeState = "" // state of git tree, either "clean" or "dirty" buildDate = "1970-01-01T00:00:00Z" // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') ) const ( // DefaultKubeBinaryVersion is the hard coded k8 binary version based on the latest K8s release. // It is supposed to be consistent with gitMajor and gitMinor, except for local tests, where gitMajor and gitMinor are "". // Should update for each minor release! DefaultKubeBinaryVersion = "1.32" ) kubernetes-component-base-1b2882b/version/dynamic.go000066400000000000000000000046471476422213000225770ustar00rootroot00000000000000/* Copyright 2023 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 version import ( "fmt" "sync/atomic" utilversion "k8s.io/apimachinery/pkg/util/version" ) var dynamicGitVersion atomic.Value func init() { // initialize to static gitVersion dynamicGitVersion.Store(gitVersion) } // SetDynamicVersion overrides the version returned as the GitVersion from Get(). // The specified version must be non-empty, a valid semantic version, and must // match the major/minor/patch version of the default gitVersion. func SetDynamicVersion(dynamicVersion string) error { if err := ValidateDynamicVersion(dynamicVersion); err != nil { return err } dynamicGitVersion.Store(dynamicVersion) return nil } // ValidateDynamicVersion ensures the given version is non-empty, a valid semantic version, // and matched the major/minor/patch version of the default gitVersion. func ValidateDynamicVersion(dynamicVersion string) error { return validateDynamicVersion(dynamicVersion, gitVersion) } func validateDynamicVersion(dynamicVersion, defaultVersion string) error { if len(dynamicVersion) == 0 { return fmt.Errorf("version must not be empty") } if dynamicVersion == defaultVersion { // allow no-op return nil } vRuntime, err := utilversion.ParseSemantic(dynamicVersion) if err != nil { return err } // must match major/minor/patch of default version var vDefault *utilversion.Version if defaultVersion == "v0.0.0-master+$Format:%H$" { // special-case the placeholder value which doesn't parse as a semantic version vDefault, err = utilversion.ParseSemantic("v0.0.0-master") } else { vDefault, err = utilversion.ParseSemantic(defaultVersion) } if err != nil { return err } if vRuntime.Major() != vDefault.Major() || vRuntime.Minor() != vDefault.Minor() || vRuntime.Patch() != vDefault.Patch() { return fmt.Errorf("version %q must match major/minor/patch of default version %q", dynamicVersion, defaultVersion) } return nil } kubernetes-component-base-1b2882b/version/verflag/000077500000000000000000000000001476422213000222375ustar00rootroot00000000000000kubernetes-component-base-1b2882b/version/verflag/verflag.go000066400000000000000000000056451476422213000242260ustar00rootroot00000000000000/* Copyright 2014 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 verflag defines utility functions to handle command line flags // related to version of Kubernetes. package verflag import ( "fmt" "io" "os" "strconv" "strings" flag "github.com/spf13/pflag" "k8s.io/component-base/version" ) type versionValue string const ( VersionFalse versionValue = "false" VersionTrue versionValue = "true" VersionRaw versionValue = "raw" ) const strRawVersion string = "raw" func (v *versionValue) IsBoolFlag() bool { return true } func (v *versionValue) Get() interface{} { return versionValue(*v) } func (v *versionValue) Set(s string) error { if s == strRawVersion { *v = VersionRaw return nil } if strings.HasPrefix(s, "v") { err := version.SetDynamicVersion(s) if err == nil { *v = versionValue(s) } return err } boolVal, err := strconv.ParseBool(s) if err == nil { if boolVal { *v = VersionTrue } else { *v = VersionFalse } } return err } func (v *versionValue) String() string { return string(*v) } // The type of the flag as required by the pflag.Value interface func (v *versionValue) Type() string { return "version" } func VersionVar(p *versionValue, name string, value versionValue, usage string) { *p = value flag.Var(p, name, usage) // "--version" will be treated as "--version=true" flag.Lookup(name).NoOptDefVal = "true" } func Version(name string, value versionValue, usage string) *versionValue { p := new(versionValue) VersionVar(p, name, value, usage) return p } const versionFlagName = "version" var ( versionFlag = Version(versionFlagName, VersionFalse, "--version, --version=raw prints version information and quits; --version=vX.Y.Z... sets the reported version") programName = "Kubernetes" ) // AddFlags registers this package's flags on arbitrary FlagSets, such that they point to the // same value as the global flags. func AddFlags(fs *flag.FlagSet) { fs.AddFlag(flag.Lookup(versionFlagName)) } // variables for unit testing PrintAndExitIfRequested var ( output = io.Writer(os.Stdout) exit = os.Exit ) // PrintAndExitIfRequested will check if --version or --version=raw was passed // and, if so, print the version and exit. func PrintAndExitIfRequested() { if *versionFlag == VersionRaw { fmt.Fprintf(output, "%#v\n", version.Get()) exit(0) } else if *versionFlag == VersionTrue { fmt.Fprintf(output, "%s %s\n", programName, version.Get()) exit(0) } } kubernetes-component-base-1b2882b/version/verflag/verflag_test.go000066400000000000000000000106421476422213000252560ustar00rootroot00000000000000/* Copyright 2023 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 verflag import ( "bytes" "fmt" "strings" "testing" "github.com/spf13/pflag" "k8s.io/component-base/version" ) func TestVersionFlag(t *testing.T) { initialFlagValue := string(*versionFlag) initialVersion := version.Get() testcases := []struct { name string flags []string expectError string expectExit bool expectPrintVersion string expectGitVersion string }{ { name: "no flag", flags: []string{}, expectGitVersion: initialVersion.GitVersion, }, { name: "false", flags: []string{"--version=false"}, expectGitVersion: initialVersion.GitVersion, }, { name: "valueless", flags: []string{"--version"}, expectGitVersion: initialVersion.GitVersion, expectExit: true, expectPrintVersion: "Kubernetes " + initialVersion.GitVersion, }, { name: "true", flags: []string{"--version=true"}, expectGitVersion: initialVersion.GitVersion, expectExit: true, expectPrintVersion: "Kubernetes " + initialVersion.GitVersion, }, { name: "raw", flags: []string{"--version=raw"}, expectGitVersion: initialVersion.GitVersion, expectExit: true, expectPrintVersion: fmt.Sprintf("%#v", initialVersion), }, { name: "truthy", flags: []string{"--version=T"}, expectGitVersion: initialVersion.GitVersion, expectExit: true, expectPrintVersion: "Kubernetes " + initialVersion.GitVersion, }, { name: "override", flags: []string{"--version=v0.0.0-custom"}, expectGitVersion: "v0.0.0-custom", }, { name: "invalid override semver", flags: []string{"--version=vX"}, expectError: `could not parse "vX"`, }, { name: "invalid override major", flags: []string{"--version=v1.0.0"}, expectError: `must match major/minor/patch`, }, { name: "invalid override minor", flags: []string{"--version=v0.1.0"}, expectError: `must match major/minor/patch`, }, { name: "invalid override patch", flags: []string{"--version=v0.0.1"}, expectError: `must match major/minor/patch`, }, { name: "override and exit", flags: []string{"--version=v0.0.0-custom", "--version"}, expectGitVersion: "v0.0.0-custom", expectExit: true, expectPrintVersion: "Kubernetes v0.0.0-custom", }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { originalOutput := output originalExit := exit outputBuffer := &bytes.Buffer{} output = outputBuffer exitCalled := false exit = func(code int) { exitCalled = true } t.Cleanup(func() { output = originalOutput exit = originalExit *versionFlag = versionValue(initialFlagValue) err := version.SetDynamicVersion(initialVersion.GitVersion) if err != nil { t.Fatal(err) } }) fs := pflag.NewFlagSet("test", pflag.ContinueOnError) AddFlags(fs) err := fs.Parse(tc.flags) if tc.expectError != "" { if err == nil { t.Fatal("expected error, got none") } if !strings.Contains(err.Error(), tc.expectError) { t.Fatalf("expected error containing %q, got %q", tc.expectError, err.Error()) } return } else if err != nil { t.Fatalf("unexpected parse error: %v", err) } if e, a := tc.expectGitVersion, version.Get().GitVersion; e != a { t.Fatalf("gitversion: expected %v, got %v", e, a) } PrintAndExitIfRequested() if e, a := tc.expectExit, exitCalled; e != a { t.Fatalf("exit(): expected %v, got %v", e, a) } if e, a := tc.expectPrintVersion, strings.TrimSpace(outputBuffer.String()); e != a { t.Fatalf("print version: expected %v, got %v", e, a) } }) } } kubernetes-component-base-1b2882b/version/version.go000066400000000000000000000171611476422213000226330ustar00rootroot00000000000000/* Copyright 2019 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 version import ( "fmt" "runtime" "sync/atomic" "k8s.io/apimachinery/pkg/util/version" apimachineryversion "k8s.io/apimachinery/pkg/version" ) type EffectiveVersion interface { BinaryVersion() *version.Version EmulationVersion() *version.Version MinCompatibilityVersion() *version.Version EqualTo(other EffectiveVersion) bool String() string Validate() []error } type MutableEffectiveVersion interface { EffectiveVersion Set(binaryVersion, emulationVersion, minCompatibilityVersion *version.Version) SetEmulationVersion(emulationVersion *version.Version) SetMinCompatibilityVersion(minCompatibilityVersion *version.Version) } type effectiveVersion struct { // When true, BinaryVersion() returns the current binary version useDefaultBuildBinaryVersion atomic.Bool // Holds the last binary version stored in Set() binaryVersion atomic.Pointer[version.Version] // If the emulationVersion is set by the users, it could only contain major and minor versions. // In tests, emulationVersion could be the same as the binary version, or set directly, // which can have "alpha" as pre-release to continue serving expired apis while we clean up the test. emulationVersion atomic.Pointer[version.Version] // minCompatibilityVersion could only contain major and minor versions. minCompatibilityVersion atomic.Pointer[version.Version] } // Get returns the overall codebase version. It's for detecting // what code a binary was built from. func Get() apimachineryversion.Info { // These variables typically come from -ldflags settings and in // their absence fallback to the settings in ./base.go return apimachineryversion.Info{ Major: gitMajor, Minor: gitMinor, GitVersion: dynamicGitVersion.Load().(string), GitCommit: gitCommit, GitTreeState: gitTreeState, BuildDate: buildDate, GoVersion: runtime.Version(), Compiler: runtime.Compiler, Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), } } func (m *effectiveVersion) BinaryVersion() *version.Version { if m.useDefaultBuildBinaryVersion.Load() { return defaultBuildBinaryVersion() } return m.binaryVersion.Load() } func (m *effectiveVersion) EmulationVersion() *version.Version { ver := m.emulationVersion.Load() if ver != nil { // Emulation version can have "alpha" as pre-release to continue serving expired apis while we clean up the test. // The pre-release should not be accessible to the users. return ver.WithPreRelease(m.BinaryVersion().PreRelease()) } return ver } func (m *effectiveVersion) MinCompatibilityVersion() *version.Version { return m.minCompatibilityVersion.Load() } func (m *effectiveVersion) EqualTo(other EffectiveVersion) bool { return m.BinaryVersion().EqualTo(other.BinaryVersion()) && m.EmulationVersion().EqualTo(other.EmulationVersion()) && m.MinCompatibilityVersion().EqualTo(other.MinCompatibilityVersion()) } func (m *effectiveVersion) String() string { if m == nil { return "" } return fmt.Sprintf("{BinaryVersion: %s, EmulationVersion: %s, MinCompatibilityVersion: %s}", m.BinaryVersion().String(), m.EmulationVersion().String(), m.MinCompatibilityVersion().String()) } func majorMinor(ver *version.Version) *version.Version { if ver == nil { return ver } return version.MajorMinor(ver.Major(), ver.Minor()) } func (m *effectiveVersion) Set(binaryVersion, emulationVersion, minCompatibilityVersion *version.Version) { m.binaryVersion.Store(binaryVersion) m.useDefaultBuildBinaryVersion.Store(false) m.emulationVersion.Store(majorMinor(emulationVersion)) m.minCompatibilityVersion.Store(majorMinor(minCompatibilityVersion)) } func (m *effectiveVersion) SetEmulationVersion(emulationVersion *version.Version) { m.emulationVersion.Store(majorMinor(emulationVersion)) } func (m *effectiveVersion) SetMinCompatibilityVersion(minCompatibilityVersion *version.Version) { m.minCompatibilityVersion.Store(majorMinor(minCompatibilityVersion)) } func (m *effectiveVersion) Validate() []error { var errs []error // Validate only checks the major and minor versions. binaryVersion := m.BinaryVersion().WithPatch(0) emulationVersion := m.emulationVersion.Load() minCompatibilityVersion := m.minCompatibilityVersion.Load() // emulationVersion can only be 1.{binaryMinor-1}...1.{binaryMinor}. maxEmuVer := binaryVersion minEmuVer := binaryVersion.SubtractMinor(1) if emulationVersion.GreaterThan(maxEmuVer) || emulationVersion.LessThan(minEmuVer) { errs = append(errs, fmt.Errorf("emulation version %s is not between [%s, %s]", emulationVersion.String(), minEmuVer.String(), maxEmuVer.String())) } // minCompatibilityVersion can only be 1.{binaryMinor-1} for alpha. maxCompVer := binaryVersion.SubtractMinor(1) minCompVer := binaryVersion.SubtractMinor(1) if minCompatibilityVersion.GreaterThan(maxCompVer) || minCompatibilityVersion.LessThan(minCompVer) { errs = append(errs, fmt.Errorf("minCompatibilityVersion version %s is not between [%s, %s]", minCompatibilityVersion.String(), minCompVer.String(), maxCompVer.String())) } return errs } func newEffectiveVersion(binaryVersion *version.Version, useDefaultBuildBinaryVersion bool) MutableEffectiveVersion { effective := &effectiveVersion{} compatVersion := binaryVersion.SubtractMinor(1) effective.Set(binaryVersion, binaryVersion, compatVersion) effective.useDefaultBuildBinaryVersion.Store(useDefaultBuildBinaryVersion) return effective } func NewEffectiveVersion(binaryVer string) MutableEffectiveVersion { if binaryVer == "" { return &effectiveVersion{} } binaryVersion := version.MustParse(binaryVer) return newEffectiveVersion(binaryVersion, false) } func defaultBuildBinaryVersion() *version.Version { verInfo := Get() return version.MustParse(verInfo.String()).WithInfo(verInfo) } // DefaultBuildEffectiveVersion returns the MutableEffectiveVersion based on the // current build information. func DefaultBuildEffectiveVersion() MutableEffectiveVersion { binaryVersion := defaultBuildBinaryVersion() if binaryVersion.Major() == 0 && binaryVersion.Minor() == 0 { return DefaultKubeEffectiveVersion() } return newEffectiveVersion(binaryVersion, true) } // DefaultKubeEffectiveVersion returns the MutableEffectiveVersion based on the // latest K8s release. func DefaultKubeEffectiveVersion() MutableEffectiveVersion { binaryVersion := version.MustParse(DefaultKubeBinaryVersion).WithInfo(Get()) return newEffectiveVersion(binaryVersion, false) } // ValidateKubeEffectiveVersion validates the EmulationVersion is equal to the binary version at 1.31 for kube components. // emulationVersion is introduced in 1.31, so it is only allowed to be equal to the binary version at 1.31. func ValidateKubeEffectiveVersion(effectiveVersion EffectiveVersion) error { binaryVersion := version.MajorMinor(effectiveVersion.BinaryVersion().Major(), effectiveVersion.BinaryVersion().Minor()) if binaryVersion.EqualTo(version.MajorMinor(1, 31)) && !effectiveVersion.EmulationVersion().EqualTo(binaryVersion) { return fmt.Errorf("emulation version needs to be equal to binary version(%s) in compatibility-version alpha, got %s", binaryVersion.String(), effectiveVersion.EmulationVersion().String()) } return nil } kubernetes-component-base-1b2882b/version/version_test.go000066400000000000000000000064651476422213000236770ustar00rootroot00000000000000/* 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 version import ( "testing" "k8s.io/apimachinery/pkg/util/version" ) func TestValidate(t *testing.T) { tests := []struct { name string binaryVersion string emulationVersion string minCompatibilityVersion string expectErrors bool }{ { name: "patch version diff ok", binaryVersion: "v1.32.2", emulationVersion: "v1.32.1", minCompatibilityVersion: "v1.31.5", }, { name: "emulation version one minor lower than binary ok", binaryVersion: "v1.32.2", emulationVersion: "v1.31.0", minCompatibilityVersion: "v1.31.0", }, { name: "emulation version two minor lower than binary not ok", binaryVersion: "v1.33.2", emulationVersion: "v1.31.0", minCompatibilityVersion: "v1.32.0", expectErrors: true, }, { name: "emulation version one minor higher than binary not ok", binaryVersion: "v1.32.2", emulationVersion: "v1.33.0", minCompatibilityVersion: "v1.31.0", expectErrors: true, }, { name: "emulation version two minor higher than binary not ok", binaryVersion: "v1.32.2", emulationVersion: "v1.34.0", minCompatibilityVersion: "v1.31.0", expectErrors: true, }, { name: "compatibility version same as binary not ok", binaryVersion: "v1.32.2", emulationVersion: "v1.32.0", minCompatibilityVersion: "v1.32.0", expectErrors: true, }, { name: "compatibility version two minor lower than binary not ok", binaryVersion: "v1.32.2", emulationVersion: "v1.32.0", minCompatibilityVersion: "v1.30.0", expectErrors: true, }, { name: "compatibility version one minor higher than binary not ok", binaryVersion: "v1.32.2", emulationVersion: "v1.32.0", minCompatibilityVersion: "v1.33.0", expectErrors: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { binaryVersion := version.MustParseGeneric(test.binaryVersion) effective := &effectiveVersion{} emulationVersion := version.MustParseGeneric(test.emulationVersion) minCompatibilityVersion := version.MustParseGeneric(test.minCompatibilityVersion) effective.Set(binaryVersion, emulationVersion, minCompatibilityVersion) errs := effective.Validate() if len(errs) > 0 && !test.expectErrors { t.Errorf("expected no errors, errors found %+v", errs) } if len(errs) == 0 && test.expectErrors { t.Errorf("expected errors, no errors found") } }) } } kubernetes-component-base-1b2882b/zpages/000077500000000000000000000000001476422213000204155ustar00rootroot00000000000000kubernetes-component-base-1b2882b/zpages/OWNERS000066400000000000000000000002741476422213000213600ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners approvers: - sig-instrumentation-approvers - logicalhan reviewers: - sig-instrumentation-reviewers labels: - sig/instrumentation kubernetes-component-base-1b2882b/zpages/README.md000066400000000000000000000007731476422213000217030ustar00rootroot00000000000000## z-pages ## Purpose The purpose of this effort is to enhance the observability, troubleshooting, and debugging capabilities of core Kubernetes components by integrating with a suite of z-pages. This will provide a standardized, low-overhead way to expose internal component metrics, runtime statistics, and configuration details, enabling users to gain deeper insights into the behavior and performance of the core components.kubernetes-component-base-1b2882b/zpages/features/000077500000000000000000000000001476422213000222335ustar00rootroot00000000000000kubernetes-component-base-1b2882b/zpages/features/doc.go000066400000000000000000000016571476422213000233400ustar00rootroot00000000000000/* 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 features contains a separate feature set specifically designed for // managing zpages related features. These feature gates control the // availability and behavior of various zpages within Kubernetes components. // New zpages added to Kubernetes components should utilize this feature set // to ensure proper management of their availability. package features kubernetes-component-base-1b2882b/zpages/features/kube_features.go000066400000000000000000000031461476422213000254120ustar00rootroot00000000000000/* 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 features import ( "k8s.io/apimachinery/pkg/util/version" "k8s.io/component-base/featuregate" ) const ( // owner: @richabanker // kep: https://kep.k8s.io/4828 ComponentFlagz featuregate.Feature = "ComponentFlagz" // owner: @richabanker // kep: https://kep.k8s.io/4827 // alpha: v1.32 // // Enables /statusz endpoint for a component making it accessible to // users with the system:monitoring cluster role. ComponentStatusz featuregate.Feature = "ComponentStatusz" ) func featureGates() map[featuregate.Feature]featuregate.VersionedSpecs { return map[featuregate.Feature]featuregate.VersionedSpecs{ ComponentFlagz: { {Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha}, }, ComponentStatusz: { {Version: version.MustParse("1.32"), Default: false, PreRelease: featuregate.Alpha}, }, } } // AddFeatureGates adds all feature gates used by this package. func AddFeatureGates(mutableFeatureGate featuregate.MutableVersionedFeatureGate) error { return mutableFeatureGate.AddVersioned(featureGates()) } kubernetes-component-base-1b2882b/zpages/flagz/000077500000000000000000000000001476422213000215205ustar00rootroot00000000000000kubernetes-component-base-1b2882b/zpages/flagz/flagreader.go000066400000000000000000000025551476422213000241520ustar00rootroot00000000000000/* 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 flagz import ( "github.com/spf13/pflag" cliflag "k8s.io/component-base/cli/flag" ) type Reader interface { GetFlagz() map[string]string } // NamedFlagSetsGetter implements Reader for cliflag.NamedFlagSets type NamedFlagSetsReader struct { FlagSets cliflag.NamedFlagSets } func (n NamedFlagSetsReader) GetFlagz() map[string]string { return convertNamedFlagSetToFlags(&n.FlagSets) } func convertNamedFlagSetToFlags(flagSets *cliflag.NamedFlagSets) map[string]string { flags := make(map[string]string) for _, fs := range flagSets.FlagSets { fs.VisitAll(func(flag *pflag.Flag) { if flag.Value != nil { value := flag.Value.String() if set, ok := flag.Annotations["classified"]; ok && len(set) > 0 { value = "CLASSIFIED" } flags[flag.Name] = value } }) } return flags } kubernetes-component-base-1b2882b/zpages/flagz/flagreader_test.go000066400000000000000000000044551476422213000252120ustar00rootroot00000000000000/* 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 flagz import ( "reflect" "testing" "github.com/spf13/pflag" "k8s.io/component-base/cli/flag" ) func TestConvertNamedFlagSetToFlags(t *testing.T) { tests := []struct { name string flagSets *flag.NamedFlagSets want map[string]string }{ { name: "basic flags", flagSets: &flag.NamedFlagSets{ FlagSets: map[string]*pflag.FlagSet{ "test": flagSet(t, map[string]flagValue{ "flag1": {value: "value1", sensitive: false}, "flag2": {value: "value2", sensitive: false}, }), }, }, want: map[string]string{ "flag1": "value1", "flag2": "value2", }, }, { name: "classified flags", flagSets: &flag.NamedFlagSets{ FlagSets: map[string]*pflag.FlagSet{ "test": flagSet(t, map[string]flagValue{ "secret1": {value: "value1", sensitive: true}, "flag2": {value: "value2", sensitive: false}, }), }, }, want: map[string]string{ "flag2": "value2", "secret1": "CLASSIFIED", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := convertNamedFlagSetToFlags(tt.flagSets) if !reflect.DeepEqual(got, tt.want) { t.Errorf("ConvertNamedFlagSetToFlags() = %v, want %v", got, tt.want) } }) } } type flagValue struct { value string sensitive bool } func flagSet(t *testing.T, flags map[string]flagValue) *pflag.FlagSet { fs := pflag.NewFlagSet("test-set", pflag.ContinueOnError) for flagName, flagVal := range flags { flagValue := "" fs.StringVar(&flagValue, flagName, flagVal.value, "test-usage") if flagVal.sensitive { err := fs.SetAnnotation(flagName, "classified", []string{"true"}) if err != nil { t.Fatalf("unexpected error when setting flag annotation: %v", err) } } } return fs } kubernetes-component-base-1b2882b/zpages/flagz/flagz.go000066400000000000000000000063141476422213000231560ustar00rootroot00000000000000/* 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 flagz import ( "bytes" "fmt" "io" "math/rand" "net/http" "sort" "strings" "sync" "github.com/munnerz/goautoneg" "k8s.io/klog/v2" ) const ( flagzHeaderFmt = ` %s flags Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only. ` ) var ( flagzSeparators = []string{":", ": ", "=", " "} errUnsupportedMediaType = fmt.Errorf("media type not acceptable, must be: text/plain") ) type registry struct { response bytes.Buffer once sync.Once } type mux interface { Handle(path string, handler http.Handler) } func Install(m mux, componentName string, flagReader Reader) { var reg registry reg.installHandler(m, componentName, flagReader) } func (reg *registry) installHandler(m mux, componentName string, flagReader Reader) { m.Handle("/flagz", reg.handleFlags(componentName, flagReader)) } func (reg *registry) handleFlags(componentName string, flagReader Reader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !acceptableMediaType(r) { http.Error(w, errUnsupportedMediaType.Error(), http.StatusNotAcceptable) return } reg.once.Do(func() { fmt.Fprintf(®.response, flagzHeaderFmt, componentName) if flagReader == nil { klog.Error("received nil flagReader") return } randomIndex := rand.Intn(len(flagzSeparators)) separator := flagzSeparators[randomIndex] // Randomize the delimiter for printing to prevent scraping of the response. printSortedFlags(®.response, flagReader.GetFlagz(), separator) }) w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, err := w.Write(reg.response.Bytes()) if err != nil { klog.Errorf("error writing response: %v", err) http.Error(w, "error writing response", http.StatusInternalServerError) } } } func acceptableMediaType(r *http.Request) bool { accepts := goautoneg.ParseAccept(r.Header.Get("Accept")) for _, accept := range accepts { if !mediaTypeMatches(accept) { continue } if len(accept.Params) == 0 { return true } if len(accept.Params) == 1 { if charset, ok := accept.Params["charset"]; ok && strings.EqualFold(charset, "utf-8") { return true } } } return false } func mediaTypeMatches(a goautoneg.Accept) bool { return (a.Type == "text" || a.Type == "*") && (a.SubType == "plain" || a.SubType == "*") } func printSortedFlags(w io.Writer, flags map[string]string, separator string) { var sortedKeys []string for key := range flags { sortedKeys = append(sortedKeys, key) } sort.Strings(sortedKeys) for _, key := range sortedKeys { fmt.Fprintf(w, "%s%s%s\n", key, separator, flags[key]) } } kubernetes-component-base-1b2882b/zpages/flagz/flagz_test.go000066400000000000000000000063551476422213000242220ustar00rootroot00000000000000/* 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 flagz import ( "fmt" "net/http" "net/http/httptest" "sort" "strings" "testing" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" cliflag "k8s.io/component-base/cli/flag" ) const wantTmpl = `%s flags Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only. ` func TestFlagz(t *testing.T) { componentName := "test-server" flagzSeparators = []string{"="} wantHeaderLines := strings.Split(fmt.Sprintf(wantTmpl, componentName), "\n") tests := []struct { name string header string flagzReader Reader wantStatus int wantResp []string }{ { name: "nil flags", wantStatus: http.StatusOK, wantResp: wantHeaderLines, }, { name: "unaccepted header", header: "some header", wantStatus: http.StatusNotAcceptable, }, { name: "test flags", flagzReader: NamedFlagSetsReader{ FlagSets: cliflag.NamedFlagSets{ FlagSets: map[string]*pflag.FlagSet{ "test": flagSet(t, map[string]flagValue{ "test-flag-bar": { value: "test-value-bar", sensitive: false, }, "test-flag-foo": { value: "test-value-foo", sensitive: false, }, }), }, }, }, wantStatus: http.StatusOK, wantResp: append(wantHeaderLines, "test-flag-bar=test-value-bar", "test-flag-foo=test-value-foo", ), }, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { mux := http.NewServeMux() Install(mux, componentName, test.flagzReader) req, err := http.NewRequest(http.MethodGet, "http://example.com/flagz", nil) if err != nil { t.Fatalf("case[%d] Unexpected error: %v", i, err) } req.Header.Set("Accept", "text/plain; charset=utf-8") if test.header != "" { req.Header.Set("Accept", test.header) } w := httptest.NewRecorder() mux.ServeHTTP(w, req) assert.Equal(t, test.wantStatus, w.Code, "case[%s] Expected status code %d, got %d", test.name, test.wantStatus, w.Code) if test.wantStatus == http.StatusOK { assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"), "case[%s] Incorrect Content-Type header", test.name) gotLines := strings.Split(w.Body.String(), "\n") gotLines = trimEmptyLines(gotLines) sort.Strings(gotLines) sort.Strings(test.wantResp) wantLines := trimEmptyLines(test.wantResp) assert.Equal(t, wantLines, gotLines, "case[%s] Response body mismatch", test.name) } }) } } func trimEmptyLines(lines []string) []string { var result []string for _, line := range lines { if line != "" { result = append(result, line) } } return result } kubernetes-component-base-1b2882b/zpages/statusz/000077500000000000000000000000001476422213000221325ustar00rootroot00000000000000kubernetes-component-base-1b2882b/zpages/statusz/registry.go000066400000000000000000000034431476422213000243350ustar00rootroot00000000000000/* 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 statusz import ( "time" "k8s.io/apimachinery/pkg/util/version" "k8s.io/component-base/featuregate" "k8s.io/klog/v2" compbasemetrics "k8s.io/component-base/metrics" utilversion "k8s.io/component-base/version" ) type statuszRegistry interface { processStartTime() time.Time goVersion() string binaryVersion() *version.Version emulationVersion() *version.Version } type registry struct{} func (registry) processStartTime() time.Time { start, err := compbasemetrics.GetProcessStart() if err != nil { klog.Errorf("Could not get process start time, %v", err) } return time.Unix(int64(start), 0) } func (registry) goVersion() string { return utilversion.Get().GoVersion } func (registry) binaryVersion() *version.Version { effectiveVer := featuregate.DefaultComponentGlobalsRegistry.EffectiveVersionFor(featuregate.DefaultKubeComponent) if effectiveVer != nil { return effectiveVer.BinaryVersion() } return utilversion.DefaultKubeEffectiveVersion().BinaryVersion() } func (registry) emulationVersion() *version.Version { effectiveVer := featuregate.DefaultComponentGlobalsRegistry.EffectiveVersionFor(featuregate.DefaultKubeComponent) if effectiveVer != nil { return effectiveVer.EmulationVersion() } return nil } kubernetes-component-base-1b2882b/zpages/statusz/registry_test.go000066400000000000000000000063361476422213000254000ustar00rootroot00000000000000/* 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 statusz import ( "testing" "github.com/stretchr/testify/assert" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/version" "k8s.io/component-base/featuregate" utilversion "k8s.io/component-base/version" ) func TestBinaryVersion(t *testing.T) { componentGlobalsRegistry := featuregate.DefaultComponentGlobalsRegistry tests := []struct { name string setFakeEffectiveVersion bool fakeVersion string wantBinaryVersion *version.Version }{ { name: "binaryVersion with effective version", wantBinaryVersion: version.MustParseSemantic("v1.2.3"), setFakeEffectiveVersion: true, fakeVersion: "1.2.3", }, { name: "binaryVersion without effective version", wantBinaryVersion: utilversion.DefaultKubeEffectiveVersion().BinaryVersion(), }, } for _, tt := range tests { componentGlobalsRegistry.Reset() t.Run(tt.name, func(t *testing.T) { if tt.setFakeEffectiveVersion { verKube := utilversion.NewEffectiveVersion(tt.fakeVersion) fg := featuregate.NewVersionedFeatureGate(version.MustParse(tt.fakeVersion)) utilruntime.Must(componentGlobalsRegistry.Register(featuregate.DefaultKubeComponent, verKube, fg)) } registry := ®istry{} got := registry.binaryVersion() assert.Equal(t, tt.wantBinaryVersion, got) }) } } func TestEmulationVersion(t *testing.T) { componentGlobalsRegistry := featuregate.DefaultComponentGlobalsRegistry tests := []struct { name string setFakeEffectiveVersion bool fakeEmulVer string wantEmul *version.Version }{ { name: "emulationVersion with effective version", fakeEmulVer: "2.3.4", setFakeEffectiveVersion: true, wantEmul: version.MustParseSemantic("2.3.4"), }, { name: "emulationVersion without effective version", wantEmul: nil, }, } for _, tt := range tests { componentGlobalsRegistry.Reset() t.Run(tt.name, func(t *testing.T) { if tt.setFakeEffectiveVersion { verKube := utilversion.NewEffectiveVersion("0.0.0") verKube.SetEmulationVersion(version.MustParse(tt.fakeEmulVer)) fg := featuregate.NewVersionedFeatureGate(version.MustParse(tt.fakeEmulVer)) utilruntime.Must(componentGlobalsRegistry.Register(featuregate.DefaultKubeComponent, verKube, fg)) } registry := ®istry{} got := registry.emulationVersion() if tt.wantEmul != nil && got != nil { assert.Equal(t, tt.wantEmul.Major(), got.Major()) assert.Equal(t, tt.wantEmul.Minor(), got.Minor()) } else { assert.Equal(t, tt.wantEmul, got) } }) } } kubernetes-component-base-1b2882b/zpages/statusz/statusz.go000066400000000000000000000102351476422213000241770ustar00rootroot00000000000000/* 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 statusz import ( "bytes" "fmt" "html/template" "math/rand" "net/http" "strings" "time" "github.com/munnerz/goautoneg" "k8s.io/klog/v2" ) var ( delimiters = []string{":", ": ", "=", " "} errUnsupportedMediaType = fmt.Errorf("media type not acceptable, must be: text/plain") ) const ( headerFmt = ` %s statusz Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only. ` dataTemplate = ` Started{{.Delim}} {{.StartTime}} Up{{.Delim}} {{.Uptime}} Go version{{.Delim}} {{.GoVersion}} Binary version{{.Delim}} {{.BinaryVersion}} {{if .EmulationVersion}}Emulation version{{.Delim}} {{.EmulationVersion}}{{end}} ` ) type contentFields struct { Delim string StartTime string Uptime string GoVersion string BinaryVersion string EmulationVersion string } type mux interface { Handle(path string, handler http.Handler) } func NewRegistry() statuszRegistry { return registry{} } func Install(m mux, componentName string, reg statuszRegistry) { dataTmpl, err := initializeTemplates() if err != nil { klog.Errorf("error while parsing gotemplates: %v", err) return } m.Handle("/statusz", handleStatusz(componentName, dataTmpl, reg)) } func initializeTemplates() (*template.Template, error) { d := template.New("data") dataTmpl, err := d.Parse(dataTemplate) if err != nil { return nil, err } return dataTmpl, nil } func handleStatusz(componentName string, dataTmpl *template.Template, reg statuszRegistry) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !acceptableMediaType(r) { http.Error(w, errUnsupportedMediaType.Error(), http.StatusNotAcceptable) return } fmt.Fprintf(w, headerFmt, componentName) data, err := populateStatuszData(dataTmpl, reg) if err != nil { klog.Errorf("error while populating statusz data: %v", err) http.Error(w, "error while populating statusz data", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprint(w, data) } } // TODO(richabanker) : Move this to a common place to be reused for all zpages. func acceptableMediaType(r *http.Request) bool { accepts := goautoneg.ParseAccept(r.Header.Get("Accept")) for _, accept := range accepts { if !mediaTypeMatches(accept) { continue } if len(accept.Params) == 0 { return true } if len(accept.Params) == 1 { if charset, ok := accept.Params["charset"]; ok && strings.EqualFold(charset, "utf-8") { return true } } } return false } func mediaTypeMatches(a goautoneg.Accept) bool { return (a.Type == "text" || a.Type == "*") && (a.SubType == "plain" || a.SubType == "*") } func populateStatuszData(tmpl *template.Template, reg statuszRegistry) (string, error) { if tmpl == nil { return "", fmt.Errorf("received nil template") } randomIndex := rand.Intn(len(delimiters)) data := contentFields{ Delim: delimiters[randomIndex], StartTime: reg.processStartTime().Format(time.UnixDate), Uptime: uptime(reg.processStartTime()), GoVersion: reg.goVersion(), BinaryVersion: reg.binaryVersion().String(), } if reg.emulationVersion() != nil { data.EmulationVersion = reg.emulationVersion().String() } var tpl bytes.Buffer err := tmpl.Execute(&tpl, data) if err != nil { return "", fmt.Errorf("error executing statusz template: %w", err) } return tpl.String(), nil } func uptime(t time.Time) string { upSince := int64(time.Since(t).Seconds()) return fmt.Sprintf("%d hr %02d min %02d sec", upSince/3600, (upSince/60)%60, upSince%60) } kubernetes-component-base-1b2882b/zpages/statusz/statusz_test.go000066400000000000000000000127671476422213000252520ustar00rootroot00000000000000/* 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 statusz import ( "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/util/version" ) const wantTmpl = ` %s statusz Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only. Started: %v Up: %s Go version: %s Binary version: %v Emulation version: %v ` const wantTmplWithoutEmulation = ` %s statusz Warning: This endpoint is not meant to be machine parseable, has no formatting compatibility guarantees and is for debugging purposes only. Started: %v Up: %s Go version: %s Binary version: %v ` func TestStatusz(t *testing.T) { delimiters = []string{":"} fakeStartTime := time.Now() fakeUptime := uptime(fakeStartTime) fakeGoVersion := "1.21" fakeBvStr := "1.31" fakeEvStr := "1.30" fakeBinaryVersion := parseVersion(t, fakeBvStr) fakeEmulationVersion := parseVersion(t, fakeEvStr) tests := []struct { name string componentName string reqHeader string registry fakeRegistry wantStatusCode int wantBody string }{ { name: "invalid header", reqHeader: "some header", wantStatusCode: http.StatusNotAcceptable, }, { name: "valid request", componentName: "test-server", reqHeader: "text/plain; charset=utf-8", registry: fakeRegistry{ startTime: fakeStartTime, goVer: fakeGoVersion, binaryVer: fakeBinaryVersion, emulationVer: fakeEmulationVersion, }, wantStatusCode: http.StatusOK, wantBody: fmt.Sprintf( wantTmpl, "test-server", fakeStartTime.Format(time.UnixDate), fakeUptime, fakeGoVersion, fakeBinaryVersion, fakeEmulationVersion, ), }, { name: "missing emulation version", componentName: "test-server", reqHeader: "text/plain; charset=utf-8", registry: fakeRegistry{ startTime: fakeStartTime, goVer: fakeGoVersion, binaryVer: fakeBinaryVersion, emulationVer: nil, }, wantStatusCode: http.StatusOK, wantBody: fmt.Sprintf( wantTmplWithoutEmulation, "test-server", fakeStartTime.Format(time.UnixDate), fakeUptime, fakeGoVersion, fakeBinaryVersion, ), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mux := http.NewServeMux() Install(mux, tt.componentName, tt.registry) path := "/statusz" req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://example.com%s", path), nil) if err != nil { t.Fatalf("unexpected error while creating request: %v", err) } req.Header.Set("Accept", "text/plain; charset=utf-8") if tt.reqHeader != "" { req.Header.Set("Accept", tt.reqHeader) } w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != tt.wantStatusCode { t.Fatalf("want status code: %v, got: %v", tt.wantStatusCode, w.Code) } if tt.wantStatusCode == http.StatusOK { c := w.Header().Get("Content-Type") if c != "text/plain; charset=utf-8" { t.Fatalf("want header: %v, got: %v", "text/plain", c) } if diff := cmp.Diff(tt.wantBody, string(w.Body.String())); diff != "" { t.Errorf("Unexpected diff on response (-want,+got):\n%s", diff) } } }) } } func TestAcceptableMediaTypes(t *testing.T) { tests := []struct { name string reqHeader string want bool }{ { name: "valid text/plain header", reqHeader: "text/plain", want: true, }, { name: "valid text/* header", reqHeader: "text/*", want: true, }, { name: "valid */plain header", reqHeader: "*/plain", want: true, }, { name: "valid accept args", reqHeader: "text/plain; charset=utf-8", want: true, }, { name: "invalid text/foo header", reqHeader: "text/foo", want: false, }, { name: "invalid text/plain params", reqHeader: "text/plain; foo=bar", want: false, }, } for _, tt := range tests { req, err := http.NewRequest(http.MethodGet, "http://example.com/statusz", nil) if err != nil { t.Fatalf("Unexpected error while creating request: %v", err) } req.Header.Set("Accept", tt.reqHeader) got := acceptableMediaType(req) if got != tt.want { t.Errorf("Unexpected response from acceptableMediaType(), want %v, got = %v", tt.want, got) } } } func parseVersion(t *testing.T, v string) *version.Version { parsed, err := version.ParseMajorMinor(v) if err != nil { t.Fatalf("error parsing binary version: %s", v) } return parsed } type fakeRegistry struct { startTime time.Time goVer string binaryVer *version.Version emulationVer *version.Version } func (f fakeRegistry) processStartTime() time.Time { return f.startTime } func (f fakeRegistry) goVersion() string { return f.goVer } func (f fakeRegistry) binaryVersion() *version.Version { return f.binaryVer } func (f fakeRegistry) emulationVersion() *version.Version { return f.emulationVer }