auth: OpenID/OAUth2 middleware

2 configurations for the listeners are now possible:
- enableJWT=false with client ssl auth
- enableJWT=true with https

Actual verification of the tokens is handled by
https://github.com/openshift-online/ocm-sdk-go.

An authentication handler is run as the top level handler, before any
routing is done. Routes which do not require authentication should be
listed as exceptions.

Authentication can be restricted using an ACL file which allows
filtering based on JWT claims. For more information see the inline
comments in ocm-sdk/authentication.

As an added quirk the `-v` flag for the osbuild-composer executable was
changed to `-verbose` to avoid flag collision with glog which declares
the `-v` flag in the package `init()` function. The ocm-sdk depends on
glog and pulls it in.
This commit is contained in:
sanne 2021-08-05 16:56:10 +02:00 committed by Tom Gundersen
parent 58613788bc
commit 4a057bf3d5
192 changed files with 25042 additions and 110 deletions

View file

@ -0,0 +1,177 @@
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

View file

@ -0,0 +1,67 @@
/*
Copyright (c) 2019 Red Hat, Inc.
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.
*/
// This file contains functions that extract information from the context.
package authentication
import (
"context"
"fmt"
"github.com/dgrijalva/jwt-go"
)
// ContextWithToken creates a new context containing the given token.
func ContextWithToken(parent context.Context, token *jwt.Token) context.Context {
return context.WithValue(parent, tokenKeyValue, token)
}
// TokenFromContext extracts the JSON web token of the user from the context. If no token is found
// in the context then the result will be nil.
func TokenFromContext(ctx context.Context) (result *jwt.Token, err error) {
switch token := ctx.Value(tokenKeyValue).(type) {
case nil:
case *jwt.Token:
result = token
default:
err = fmt.Errorf(
"expected a token in the '%s' context value, but got '%T'",
tokenKeyValue, token,
)
}
return
}
// BearerFromContext extracts the bearer token of the user from the context. If no user is found in
// the context then the result will be the empty string.
func BearerFromContext(ctx context.Context) (result string, err error) {
token, err := TokenFromContext(ctx)
if err != nil {
return
}
if token == nil {
return
}
result = token.Raw
return
}
// tokenKeyType is the type of the key used to store the token in the context.
type tokenKeyType string
// tokenKeyValue is the key used to store the token in the context:
const tokenKeyValue tokenKeyType = "token"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,57 @@
/*
Copyright (c) 2021 Red Hat, Inc.
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.
*/
// This file contains helper functions used in several places in the package.
package authentication
import (
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
)
// tokenRemaining determines if the given token will eventually expire (offile access tokens, for
// example, never expire) and the time till it expires. That time will be positive if the token
// isn't expired, and negative if the token has already expired.
//
// For tokens that don't have the `exp` claim, or that have it with value zero (typical for offline
// access tokens) the result will always be `false` and zero.
func tokenRemaining(token *jwt.Token, now time.Time) (expires bool, duration time.Duration,
err error) {
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
err = fmt.Errorf("expected map claims but got %T", claims)
return
}
var exp float64
claim, ok := claims["exp"]
if !ok {
return
}
exp, ok = claim.(float64)
if !ok {
err = fmt.Errorf("expected floating point 'exp' but got %T", claim)
return
}
if exp == 0 {
return
}
duration = time.Unix(int64(exp), 0).Sub(now)
expires = true
return
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,416 @@
/*
Copyright (c) 2020 Red Hat, Inc.
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.
*/
// IMPORTANT: This file has been generated automatically, refrain from modifying it manually as all
// your changes will be lost when the file is generated again.
package errors // github.com/openshift-online/ocm-sdk-go/errors
import (
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/golang/glog"
jsoniter "github.com/json-iterator/go"
"github.com/openshift-online/ocm-sdk-go/helpers"
)
// Error kind is the name of the type used to represent errors.
const ErrorKind = "Error"
// ErrorNilKind is the name of the type used to nil errors.
const ErrorNilKind = "ErrorNil"
// ErrorBuilder is a builder for the error type.
type ErrorBuilder struct {
bitmap_ uint32
id string
href string
code string
reason string
operationID string
}
// Error represents errors.
type Error struct {
bitmap_ uint32
id string
href string
code string
reason string
operationID string
}
// NewError creates a new builder that can then be used to create error objects.
func NewError() *ErrorBuilder {
return &ErrorBuilder{}
}
// ID sets the identifier of the error.
func (b *ErrorBuilder) ID(value string) *ErrorBuilder {
b.id = value
b.bitmap_ |= 1
return b
}
// HREF sets the link of the error.
func (b *ErrorBuilder) HREF(value string) *ErrorBuilder {
b.href = value
b.bitmap_ |= 2
return b
}
// Code sets the code of the error.
func (b *ErrorBuilder) Code(value string) *ErrorBuilder {
b.code = value
b.bitmap_ |= 4
return b
}
// Reason sets the reason of the error.
func (b *ErrorBuilder) Reason(value string) *ErrorBuilder {
b.reason = value
b.bitmap_ |= 8
return b
}
// OperationID sets the identifier of the operation that caused the error.
func (b *ErrorBuilder) OperationID(value string) *ErrorBuilder {
b.operationID = value
b.bitmap_ |= 16
return b
}
// Build uses the information stored in the builder to create a new error object.
func (b *ErrorBuilder) Build() (result *Error, err error) {
result = &Error{
id: b.id,
href: b.href,
code: b.code,
reason: b.reason,
operationID: b.operationID,
bitmap_: b.bitmap_,
}
return
}
// Kind returns the name of the type of the error.
func (e *Error) Kind() string {
if e == nil {
return ErrorNilKind
}
return ErrorKind
}
// ID returns the identifier of the error.
func (e *Error) ID() string {
if e != nil && e.bitmap_&1 != 0 {
return e.id
}
return ""
}
// GetID returns the identifier of the error and a flag indicating if the
// identifier has a value.
func (e *Error) GetID() (value string, ok bool) {
ok = e != nil && e.bitmap_&1 != 0
if ok {
value = e.id
}
return
}
// HREF returns the link to the error.
func (e *Error) HREF() string {
if e != nil && e.bitmap_&2 != 0 {
return e.href
}
return ""
}
// GetHREF returns the link of the error and a flag indicating if the
// link has a value.
func (e *Error) GetHREF() (value string, ok bool) {
ok = e != nil && e.bitmap_&2 != 0
if ok {
value = e.href
}
return
}
// Code returns the code of the error.
func (e *Error) Code() string {
if e != nil && e.bitmap_&4 != 0 {
return e.code
}
return ""
}
// GetCode returns the link of the error and a flag indicating if the
// code has a value.
func (e *Error) GetCode() (value string, ok bool) {
ok = e != nil && e.bitmap_&4 != 0
if ok {
value = e.code
}
return
}
// Reason returns the reason of the error.
func (e *Error) Reason() string {
if e != nil && e.bitmap_&8 != 0 {
return e.reason
}
return ""
}
// GetReason returns the link of the error and a flag indicating if the
// reason has a value.
func (e *Error) GetReason() (value string, ok bool) {
ok = e != nil && e.bitmap_&8 != 0
if ok {
value = e.reason
}
return
}
// OperationID returns the identifier of the operation that caused the error.
func (e *Error) OperationID() string {
if e != nil && e.bitmap_&16 != 0 {
return e.operationID
}
return ""
}
// GetOperationID returns the identifier of the operation that caused the error and
// a flag indicating if that identifier does have a value.
func (e *Error) GetOperationID() (value string, ok bool) {
ok = e != nil && e.bitmap_&16 != 0
if ok {
value = e.operationID
}
return
}
// Error is the implementation of the error interface.
func (e *Error) Error() string {
chunks := make([]string, 0, 3)
if e.id != "" {
chunks = append(chunks, fmt.Sprintf("identifier is '%s'", e.id))
}
if e.code != "" {
chunks = append(chunks, fmt.Sprintf("code is '%s'", e.code))
}
if e.operationID != "" {
chunks = append(chunks, fmt.Sprintf("operation identifier is '%s'", e.operationID))
}
var result string
size := len(chunks)
if size == 1 {
result = chunks[0]
} else if size > 1 {
result = strings.Join(chunks[0:size-1], ", ") + " and " + chunks[size-1]
}
if e.reason != "" {
if result != "" {
result = result + ": "
}
result = result + e.reason
}
if result == "" {
result = "unknown error"
}
return result
}
// String returns a string representing the error.
func (e *Error) String() string {
return e.Error()
}
// UnmarshalError reads an error from the given source which can be an slice of
// bytes, a string, a reader or a JSON decoder.
func UnmarshalError(source interface{}) (object *Error, err error) {
iterator, err := helpers.NewIterator(source)
if err != nil {
return
}
object = readError(iterator)
err = iterator.Error
return
}
func readError(iterator *jsoniter.Iterator) *Error {
object := &Error{}
for {
field := iterator.ReadObject()
if field == "" {
break
}
switch field {
case "id":
object.id = iterator.ReadString()
object.bitmap_ |= 1
case "href":
object.href = iterator.ReadString()
object.bitmap_ |= 2
case "code":
object.code = iterator.ReadString()
object.bitmap_ |= 4
case "reason":
object.reason = iterator.ReadString()
object.bitmap_ |= 8
case "operation_id":
object.operationID = iterator.ReadString()
object.bitmap_ |= 16
default:
iterator.ReadAny()
}
}
return object
}
// MarshalError writes an error to the given writer.
func MarshalError(e *Error, writer io.Writer) error {
stream := helpers.NewStream(writer)
writeError(e, stream)
stream.Flush()
return stream.Error
}
func writeError(e *Error, stream *jsoniter.Stream) {
stream.WriteObjectStart()
stream.WriteObjectField("kind")
stream.WriteString(ErrorKind)
if e.bitmap_&1 != 0 {
stream.WriteMore()
stream.WriteObjectField("id")
stream.WriteString(e.id)
}
if e.bitmap_&2 != 0 {
stream.WriteMore()
stream.WriteObjectField("href")
stream.WriteString(e.href)
}
if e.bitmap_&4 != 0 {
stream.WriteMore()
stream.WriteObjectField("code")
stream.WriteString(e.code)
}
if e.bitmap_&8 != 0 {
stream.WriteMore()
stream.WriteObjectField("reason")
stream.WriteString(e.reason)
}
if e.bitmap_&16 != 0 {
stream.WriteMore()
stream.WriteObjectField("operation_id")
stream.WriteString(e.operationID)
}
stream.WriteObjectEnd()
}
var panicID = "1000"
var panicError, _ = NewError().
ID(panicID).
Reason("An unexpected error happened, please check the log of the service " +
"for details").
Build()
// SendError writes a given error and status code to a response writer.
// if an error occurred it will log the error and exit.
// This methods is used internaly and no backwards compatibily is guaranteed.
func SendError(w http.ResponseWriter, r *http.Request, object *Error) {
status, err := strconv.Atoi(object.ID())
if err != nil {
SendPanic(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
err = MarshalError(object, w)
if err != nil {
glog.Errorf("Can't send response body for request '%s'", r.URL.Path)
return
}
}
// SendPanic sends a panic error response to the client, but it doesn't end the process.
// This methods is used internaly and no backwards compatibily is guaranteed.
func SendPanic(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
err := MarshalError(panicError, w)
if err != nil {
glog.Errorf(
"Can't send panic response for request '%s': %s",
r.URL.Path,
err.Error(),
)
}
}
// SendNotFound sends a generic 404 error.
func SendNotFound(w http.ResponseWriter, r *http.Request) {
reason := fmt.Sprintf(
"Can't find resource for path '%s'",
r.URL.Path,
)
body, err := NewError().
ID("404").
Reason(reason).
Build()
if err != nil {
SendPanic(w, r)
return
}
SendError(w, r, body)
}
// SendMethodNotAllowed sends a generic 405 error.
func SendMethodNotAllowed(w http.ResponseWriter, r *http.Request) {
reason := fmt.Sprintf(
"Method '%s' isn't supported for path '%s'",
r.Method, r.URL.Path,
)
body, err := NewError().
ID("405").
Reason(reason).
Build()
if err != nil {
SendPanic(w, r)
return
}
SendError(w, r, body)
}
// SendInternalServerError sends a generic 500 error.
func SendInternalServerError(w http.ResponseWriter, r *http.Request) {
reason := fmt.Sprintf(
"Can't process '%s' request for path '%s' due to an internal"+
"server error",
r.Method, r.URL.Path,
)
body, err := NewError().
ID("500").
Reason(reason).
Build()
if err != nil {
SendPanic(w, r)
return
}
SendError(w, r, body)
}

View file

@ -0,0 +1,170 @@
/*
Copyright (c) 2020 Red Hat, Inc.
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.
*/
// IMPORTANT: This file has been generated automatically, refrain from modifying it manually as all
// your changes will be lost when the file is generated again.
package helpers // github.com/openshift-online/ocm-sdk-go/helpers
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
// AddValue creates the given set of query parameters if needed, an then adds
// the given parameter.
func AddValue(query *url.Values, name string, value interface{}) {
if *query == nil {
*query = make(url.Values)
}
query.Add(name, fmt.Sprintf("%v", value))
}
// CopyQuery creates a copy of the given set of query parameters.
func CopyQuery(query url.Values) url.Values {
if query == nil {
return nil
}
result := make(url.Values)
for name, values := range query {
result[name] = CopyValues(values)
}
return result
}
// AddHeader creates the given set of headers if needed, and then adds the given
// header:
func AddHeader(header *http.Header, name string, value interface{}) {
if *header == nil {
*header = make(http.Header)
}
header.Add(name, fmt.Sprintf("%v", value))
}
// CopyHeader creates a copy of the given set of headers.
func CopyHeader(header http.Header) http.Header {
result := make(http.Header)
for name, values := range header {
result[name] = CopyValues(values)
}
return result
}
// CopyValues copies a slice of strings.
func CopyValues(values []string) []string {
if values == nil {
return nil
}
result := make([]string, len(values))
copy(result, values)
return result
}
// Segments calculates the path segments for the given path.
func Segments(path string) []string {
for strings.HasPrefix(path, "/") {
path = path[1:]
}
for strings.HasSuffix(path, "/") {
path = path[0 : len(path)-1]
}
return strings.Split(path, "/")
}
// PollContext repeatedly executes a task till it returns one of the given statuses and till the result
// satisfies all the given predicates.
func PollContext(
ctx context.Context,
interval time.Duration,
statuses []int,
predicates []func(interface{}) bool,
task func(context.Context) (int, interface{}, error),
) (result interface{}, err error) {
// Check the deadline:
deadline, ok := ctx.Deadline()
if !ok {
err = fmt.Errorf("context deadline is mandatory")
return
}
// Check the interval:
if interval <= 0 {
err = fmt.Errorf("interval must be greater than zero")
return
}
// Create a cancellable context so that we can explicitly cancel it when we know that the next
// iteration of the loop will be after the deadline:
ctx, cancel := context.WithCancel(ctx)
// If no expected status has been explicitly specified then add the default:
if len(statuses) == 0 {
statuses = []int{http.StatusOK}
}
for {
// Execute the task. If this produces an error and the status code is zero it means that
// there was an error like a timeout, or a low level communications problem. In that
// case we want to immediately stop waiting.
var status int
status, result, err = task(ctx)
if err != nil && status == 0 {
break
}
// Evaluate the status and the predicates:
statusOK := evalStatus(statuses, status)
predicatesOK := evalPredicates(predicates, result)
if statusOK && predicatesOK {
break
}
// If either the status or the predicates aren't acceptable then we need to check if we
// have enough time for another iteration before the deadline:
if time.Now().Add(interval).After(deadline) {
cancel()
break
}
time.Sleep(interval)
}
return
}
// evalStatus checks if the actual status is one of the expected ones.
func evalStatus(expected []int, actual int) bool {
for _, current := range expected {
if actual == current {
return true
}
}
return false
}
// evalPredicates checks if the object satisfies all the predicates.
func evalPredicates(predicates []func(interface{}) bool, object interface{}) bool {
if len(predicates) > 0 && object == nil {
return false
}
for _, predicate := range predicates {
if !predicate(object) {
return false
}
}
return true
}

View file

@ -0,0 +1,208 @@
/*
Copyright (c) 2020 Red Hat, Inc.
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.
*/
// IMPORTANT: This file has been generated automatically, refrain from modifying it manually as all
// your changes will be lost when the file is generated again.
package helpers // github.com/openshift-online/ocm-sdk-go/helpers
import (
"fmt"
"io"
"net/url"
"strconv"
"time"
jsoniter "github.com/json-iterator/go"
)
// NewIterator creates a new JSON iterator that will read to the given source, which
// can be a slice of bytes, a string or a reader.
func NewIterator(source interface{}) (iterator *jsoniter.Iterator, err error) {
config := jsoniter.Config{}
api := config.Froze()
switch typed := source.(type) {
case []byte:
iterator = jsoniter.ParseBytes(api, typed)
case string:
iterator = jsoniter.ParseString(api, typed)
case io.Reader:
iterator = jsoniter.Parse(api, typed, 4096)
default:
err = fmt.Errorf(
"expected slice of bytes, string or reader but got '%T'",
source,
)
}
return
}
// NewStream creates a new JSON stream that will write to the given writer.
func NewStream(writer io.Writer) *jsoniter.Stream {
config := jsoniter.Config{
IndentionStep: 2,
}
api := config.Froze()
return jsoniter.NewStream(api, writer, 0)
}
// NewBoolean allocates a new bool in the heap and returns a pointer to it.
func NewBoolean(value bool) *bool {
return &value
}
// NewInteger allocates a new integer in the heap and returns a pointer to it.
func NewInteger(value int) *int {
return &value
}
// NewFloat allocates a new floating point value in the heap and returns an pointer
// to it.
func NewFloat(value float64) *float64 {
return &value
}
// NewString allocates a new string in the heap and returns a pointer to it.
func NewString(value string) *string {
return &value
}
// NewDate allocates a new date in the heap and returns a pointer to it.
func NewDate(value time.Time) *time.Time {
return &value
}
// ParseInteger reads a string and parses it to integer,
// if an error occurred it returns a non-nil error.
func ParseInteger(query url.Values, parameterName string) (*int, error) {
values := query[parameterName]
count := len(values)
if count == 0 {
return nil, nil
}
if count > 1 {
err := fmt.Errorf(
"expected at most one value for parameter '%s' but got %d",
parameterName, count,
)
return nil, err
}
value := values[0]
parsedInt64, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, fmt.Errorf(
"value '%s' isn't valid for the '%s' parameter because it isn't an integer: %v",
value, parameterName, err,
)
}
parsedInt := int(parsedInt64)
return &parsedInt, nil
}
// ParseFloat reads a string and parses it to float,
// if an error occurred it returns a non-nil error.
func ParseFloat(query url.Values, parameterName string) (*float64, error) {
values := query[parameterName]
count := len(values)
if count == 0 {
return nil, nil
}
if count > 1 {
err := fmt.Errorf(
"expected at most one value for parameter '%s' but got %d",
parameterName, count,
)
return nil, err
}
value := values[0]
parsedFloat, err := strconv.ParseFloat(value, 64)
if err != nil {
return nil, fmt.Errorf(
"value '%s' isn't valid for the '%s' parameter because it isn't a float: %v",
value, parameterName, err,
)
}
return &parsedFloat, nil
}
// ParseString returns a pointer to the string and nil error.
func ParseString(query url.Values, parameterName string) (*string, error) {
values := query[parameterName]
count := len(values)
if count == 0 {
return nil, nil
}
if count > 1 {
err := fmt.Errorf(
"expected at most one value for parameter '%s' but got %d",
parameterName, count,
)
return nil, err
}
return &values[0], nil
}
// ParseBoolean reads a string and parses it to boolean,
// if an error occurred it returns a non-nil error.
func ParseBoolean(query url.Values, parameterName string) (*bool, error) {
values := query[parameterName]
count := len(values)
if count == 0 {
return nil, nil
}
if count > 1 {
err := fmt.Errorf(
"expected at most one value for parameter '%s' but got %d",
parameterName, count,
)
return nil, err
}
value := values[0]
parsedBool, err := strconv.ParseBool(value)
if err != nil {
return nil, fmt.Errorf(
"value '%s' isn't valid for the '%s' parameter because it isn't a boolean: %v",
value, parameterName, err,
)
}
return &parsedBool, nil
}
// ParseDate reads a string and parses it to a time.Time,
// if an error occurred it returns a non-nil error.
func ParseDate(query url.Values, parameterName string) (*time.Time, error) {
values := query[parameterName]
count := len(values)
if count == 0 {
return nil, nil
}
if count > 1 {
err := fmt.Errorf(
"expected at most one value for parameter '%s' but got %d",
parameterName, count,
)
return nil, err
}
value := values[0]
parsedTime, err := time.Parse(time.RFC3339, value)
if err != nil {
return nil, fmt.Errorf(
"value '%s' isn't valid for the '%s' parameter because it isn't a date: %v",
value, parameterName, err,
)
}
return &parsedTime, nil
}

View file

@ -0,0 +1,91 @@
/*
Copyright (c) 2021 Red Hat, Inc.
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.
*/
// This file contains the functions used to check content types.
package internal
import (
"fmt"
"html"
"io/ioutil"
"mime"
"net/http"
"regexp"
"strings"
strip "github.com/grokify/html-strip-tags-go"
)
var wsRegex = regexp.MustCompile(`\s+`)
// CheckContentType checks that the content type of the given response is JSON. Note that if the
// content type isn't JSON this method will consume the complete body in order to generate an error
// message containing a summary of the content.
func CheckContentType(response *http.Response) error {
var err error
var mediaType string
contentType := response.Header.Get("Content-Type")
if contentType != "" {
mediaType, _, err = mime.ParseMediaType(contentType)
if err != nil {
return err
}
} else {
mediaType = contentType
}
if !strings.EqualFold(mediaType, "application/json") {
var summary string
summary, err = contentSummary(mediaType, response)
if err != nil {
return fmt.Errorf(
"expected response content type 'application/json' but received "+
"'%s' and couldn't obtain content summary: %w",
mediaType, err,
)
}
return fmt.Errorf(
"expected response content type 'application/json' but received '%s' and "+
"content '%s'",
mediaType, summary,
)
}
return nil
}
// contentSummary reads the body of the given response and returns a summary it. The summary will
// be the complete body if it isn't too log. If it is too long then the summary will be the
// beginning of the content followed by ellipsis.
func contentSummary(mediaType string, response *http.Response) (summary string, err error) {
var body []byte
body, err = ioutil.ReadAll(response.Body)
if err != nil {
return
}
limit := 250
runes := []rune(string(body))
if strings.EqualFold(mediaType, "text/html") && len(runes) > limit {
content := html.UnescapeString(strip.StripTags(string(body)))
content = wsRegex.ReplaceAllString(strings.TrimSpace(content), " ")
runes = []rune(content)
}
if len(runes) > limit {
summary = fmt.Sprintf("%s...", string(runes[:limit]))
} else {
summary = string(runes)
}
return
}

View file

@ -0,0 +1,403 @@
/*
Copyright (c) 2021 Red Hat, Inc.
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.
*/
// This file contains the implementation of the object that selects the HTTP client to use to
// connect to servers using TCP or Unix sockets.
package internal
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/cookiejar"
"sync"
"golang.org/x/net/http2"
"github.com/openshift-online/ocm-sdk-go/logging"
)
// ClientSelectorBuilder contains the information and logic needed to create an HTTP client
// selector. Don't create instances of this type directly, use the NewClientSelector function.
type ClientSelectorBuilder struct {
logger logging.Logger
trustedCAs []interface{}
insecure bool
disableKeepAlives bool
transportWrappers []func(http.RoundTripper) http.RoundTripper
}
// ClientSelector contains the information needed to create select the HTTP client to use to connect
// to servers using TCP or Unix sockets.
type ClientSelector struct {
logger logging.Logger
trustedCAs *x509.CertPool
insecure bool
disableKeepAlives bool
transportWrappers []func(http.RoundTripper) http.RoundTripper
cookieJar http.CookieJar
clientsMutex *sync.Mutex
clientsTable map[string]*http.Client
}
// NewClientSelector creates a builder that can then be used to configure and create an HTTP client
// selector.
func NewClientSelector() *ClientSelectorBuilder {
return &ClientSelectorBuilder{}
}
// Logger sets the logger that will be used by the selector and by the created HTTP clients to write
// messages to the log. This is mandatory.
func (b *ClientSelectorBuilder) Logger(value logging.Logger) *ClientSelectorBuilder {
b.logger = value
return b
}
// TrustedCA sets a source that contains he certificate authorities that will be trusted by the HTTP
// clients. If this isn't explicitly specified then the clients will trust the certificate
// authorities trusted by default by the system. The value can be a *x509.CertPool or a string,
// anything else will cause an error when Build method is called. If it is a *x509.CertPool then the
// value will replace any other source given before. If it is a string then it should be the name of
// a PEM file. The contents of that file will be added to the previously given sources.
func (b *ClientSelectorBuilder) TrustedCA(value interface{}) *ClientSelectorBuilder {
if value != nil {
b.trustedCAs = append(b.trustedCAs, value)
}
return b
}
// TrustedCAs sets a list of sources that contains he certificate authorities that will be trusted
// by the HTTP clients. See the documentation of the TrustedCA method for more information about the
// accepted values.
func (b *ClientSelectorBuilder) TrustedCAs(values ...interface{}) *ClientSelectorBuilder {
for _, value := range values {
b.TrustedCA(value)
}
return b
}
// Insecure enables insecure communication with the servers. This disables verification of TLS
// certificates and host names and it isn't recommended for a production environment.
func (b *ClientSelectorBuilder) Insecure(flag bool) *ClientSelectorBuilder {
b.insecure = flag
return b
}
// DisableKeepAlives disables HTTP keep-alives with the serviers. This is unrelated to similarly
// named TCP keep-alives.
func (b *ClientSelectorBuilder) DisableKeepAlives(flag bool) *ClientSelectorBuilder {
b.disableKeepAlives = flag
return b
}
// TransportWrapper adds a function that will be used to wrap the transports of the HTTP clients. If
// used multiple times the transport wrappers will be called in the same order that they are added.
func (b *ClientSelectorBuilder) TransportWrapper(
value func(http.RoundTripper) http.RoundTripper) *ClientSelectorBuilder {
if value != nil {
b.transportWrappers = append(b.transportWrappers, value)
}
return b
}
// TransportWrappers adds a list of functions that will be used to wrap the transports of the HTTP clients.
func (b *ClientSelectorBuilder) TransportWrappers(
values ...func(http.RoundTripper) http.RoundTripper) *ClientSelectorBuilder {
for _, value := range values {
b.TransportWrapper(value)
}
return b
}
// Build uses the information stored in the builder to create a new HTTP client selector.
func (b *ClientSelectorBuilder) Build(ctx context.Context) (result *ClientSelector, err error) {
// Check parameters:
if b.logger == nil {
err = fmt.Errorf("logger is mandatory")
return
}
// Create the cookie jar:
cookieJar, err := b.createCookieJar()
if err != nil {
return
}
// Load trusted CAs:
trustedCAs, err := b.loadTrustedCAs(ctx)
if err != nil {
return
}
// Create and populate the object:
result = &ClientSelector{
logger: b.logger,
trustedCAs: trustedCAs,
insecure: b.insecure,
disableKeepAlives: b.disableKeepAlives,
transportWrappers: b.transportWrappers,
cookieJar: cookieJar,
clientsMutex: &sync.Mutex{},
clientsTable: map[string]*http.Client{},
}
return
}
func (b *ClientSelectorBuilder) loadTrustedCAs(ctx context.Context) (result *x509.CertPool,
err error) {
result, err = loadSystemCAs()
if err != nil {
return
}
for _, ca := range b.trustedCAs {
switch source := ca.(type) {
case *x509.CertPool:
b.logger.Debug(
ctx,
"Default trusted CA certificates have been explicitly replaced",
)
result = source
case string:
b.logger.Debug(
ctx,
"Loading trusted CA certificates from file '%s'",
source,
)
var buffer []byte
buffer, err = ioutil.ReadFile(source) // #nosec G304
if err != nil {
result = nil
err = fmt.Errorf(
"can't read trusted CA certificates from file '%s': %w",
source, err,
)
return
}
if !result.AppendCertsFromPEM(buffer) {
result = nil
err = fmt.Errorf(
"file '%s' doesn't contain any certificate",
source,
)
return
}
default:
result = nil
err = fmt.Errorf(
"don't know how to load trusted CA from source of type '%T'",
source,
)
return
}
}
return
}
func (b *ClientSelectorBuilder) createCookieJar() (result http.CookieJar, err error) {
result, err = cookiejar.New(nil)
return
}
// Select returns an HTTP client to use to connect to the given server address. If a client has been
// created previously for the server address it will be reused, otherwise it will be created.
func (s *ClientSelector) Select(ctx context.Context, address *ServerAddress) (client *http.Client,
err error) {
// We need to use a different client for each TCP host name and each Unix socket because we
// explicitly set the TLS server name to the host name. For example, if the first request is
// for the SSO service (it will usually be) then we would set the TLS server name to
// `sso.redhat.com`. The next API request would then use the same client and therefore it
// will use `sso.redhat.com` as the TLS server name. If the server uses SNI to select the
// certificates it will then fail because the API server doesn't have any certificate for
// `sso.redhat.com`, it will return the default certificates, and then the validation would
// fail with an error message like this:
//
// x509: certificate is valid for *.apps.app-sre-prod-04.i5h0.p1.openshiftapps.com,
// api.app-sre-prod-04.i5h0.p1.openshiftapps.com,
// rh-api.app-sre-prod-04.i5h0.p1.openshiftapps.com, not sso.redhat.com
//
// To avoid this we add the host name or socket path as a suffix to the key.
key := address.Network
switch address.Network {
case UnixNetwork:
key = fmt.Sprintf("%s:%s", key, address.Socket)
case TCPNetwork:
key = fmt.Sprintf("%s:%s", key, address.Host)
}
// We will be modifiying the clients table so we need to acquire the lock before proceeding:
s.clientsMutex.Lock()
defer s.clientsMutex.Unlock()
// Get an existing client, or create a new one if it doesn't exist yet:
client, ok := s.clientsTable[key]
if ok {
return
}
s.logger.Debug(ctx, "Client for key '%s' doesn't exist, will create it", key)
client, err = s.create(ctx, address)
if err != nil {
return
}
s.clientsTable[key] = client
return
}
// create creates a new HTTP client to use to connect to the given address.
func (s *ClientSelector) create(ctx context.Context, address *ServerAddress) (result *http.Client,
err error) {
// Create the transport:
transport, err := s.createTransport(ctx, address)
if err != nil {
return
}
// Create the client:
result = &http.Client{
Jar: s.cookieJar,
Transport: transport,
}
if s.logger.DebugEnabled() {
result.CheckRedirect = func(request *http.Request, via []*http.Request) error {
s.logger.Info(
request.Context(),
"Following redirect from '%s' to '%s'",
via[0].URL,
request.URL,
)
return nil
}
}
return
}
// createTransport creates a new HTTP transport to use to connect to the given server address.
func (s *ClientSelector) createTransport(ctx context.Context,
address *ServerAddress) (result http.RoundTripper, err error) {
// Prepare the TLS configuration:
// #nosec 402
config := &tls.Config{
ServerName: address.Host,
InsecureSkipVerify: s.insecure,
RootCAs: s.trustedCAs,
}
// Create the transport:
if address.Protocol != H2CProtocol {
// Create a regular transport. Note that this does support HTTP/2 with TLS, but
// not h2c:
transport := &http.Transport{
TLSClientConfig: config,
Proxy: http.ProxyFromEnvironment,
DisableKeepAlives: s.disableKeepAlives,
DisableCompression: false,
ForceAttemptHTTP2: true,
}
// In order to use Unix sockets we need to explicitly set dialers that use `unix` as
// network and the socket file as address, otherwise the HTTP client will always use
// `tcp` as the network and the host name from the request as the address:
if address.Network == UnixNetwork {
transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn,
error) {
dialer := net.Dialer{}
return dialer.DialContext(ctx, UnixNetwork, address.Socket)
}
transport.DialTLS = func(_, _ string) (net.Conn, error) {
// TODO: This ignores the passed context because it isn't currently
// supported. Once we migrate to Go 1.15 it should be done like
// this:
//
// transport.DialTLSContext = func(ctx context.Context,
// _, _ string) (net.Conn, error) {
// dialer := tls.Dialer{
// Config: config,
// }
// return dialer.DialContext(ctx, network, address.Socket)
// }
//
// This will only have a negative impact in applications that
// specify a deadline or timeout in the passed context, as it
// will be ignored.
return tls.Dial(UnixNetwork, address.Socket, config)
}
}
// Prepare the result:
result = transport
} else {
// In order to use h2c we need to tell the transport to allow the `http` scheme:
transport := &http2.Transport{
AllowHTTP: true,
DisableCompression: false,
}
// We also need to ignore TLS configuration when dialing, and explicitly set the
// network and socket when using Unix sockets:
if address.Network == UnixNetwork {
transport.DialTLS = func(_, _ string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(UnixNetwork, address.Socket)
}
} else {
transport.DialTLS = func(network, addr string, cfg *tls.Config) (net.Conn,
error) {
return net.Dial(network, addr)
}
}
// Prepare the result:
result = transport
}
// Transport wrappers are stored in the order that the round trippers that they create
// should be called. That means that we need to call them in reverse order.
for i := len(s.transportWrappers) - 1; i >= 0; i-- {
result = s.transportWrappers[i](result)
}
return
}
// TrustedCAs sets returns the certificate pool that contains the certificate authorities that are
// trusted by the HTTP clients.
func (s *ClientSelector) TrustedCAs() *x509.CertPool {
return s.trustedCAs
}
// Insecure returns the flag that indicates if insecure communication with the server is enabled.
func (s *ClientSelector) Insecure() bool {
return s.insecure
}
// DisableKeepAlives retursnt the flag that indicates if HTTP keep alive is disabled.
func (s *ClientSelector) DisableKeepAlives() bool {
return s.disableKeepAlives
}
// Close closes all the connections used by all the clients created by the selector.
func (s *ClientSelector) Close() error {
for _, client := range s.clientsTable {
client.CloseIdleConnections()
}
return nil
}

View file

@ -0,0 +1,26 @@
/*
Copyright (c) 2018 Red Hat, Inc.
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 internal
// TokenResponse is used to unmarshal the sub-set of properties JSON token responses that we need.
type TokenResponse struct {
AccessToken *string `json:"access_token,omitempty"`
Error *string `json:"error,omitempty"`
ErrorDescription *string `json:"error_description,omitempty"`
RefreshToken *string `json:"refresh_token,omitempty"`
TokenType *string `json:"token_type,omitempty"`
}

View file

@ -0,0 +1,93 @@
/*
Copyright (c) 2018 Red Hat, Inc.
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 internal
import (
"context"
"fmt"
"net/http"
"net/url"
"time"
)
// AddValue creates the given set of query parameters if needed, an then adds
// the given parameter.
func AddValue(query *url.Values, name string, value interface{}) {
if *query == nil {
*query = make(url.Values)
}
query.Add(name, fmt.Sprintf("%v", value))
}
// CopyQuery creates a copy of the given set of query parameters.
func CopyQuery(query url.Values) url.Values {
if query == nil {
return nil
}
result := make(url.Values)
for name, values := range query {
result[name] = CopyValues(values)
}
return result
}
// AddHeader creates the given set of headers if needed, and then adds the given
// header:
func AddHeader(header *http.Header, name string, value interface{}) {
if *header == nil {
*header = make(http.Header)
}
header.Add(name, fmt.Sprintf("%v", value))
}
// CopyHeader creates a copy of the given set of headers.
func CopyHeader(header http.Header) http.Header {
if header == nil {
return nil
}
result := make(http.Header)
for name, values := range header {
result[name] = CopyValues(values)
}
return result
}
// CopyValues copies a slice of strings.
func CopyValues(values []string) []string {
if values == nil {
return nil
}
result := make([]string, len(values))
copy(result, values)
return result
}
// SetTimeout creates the given context, if needed, and sets the given timeout.
func SetTimeout(ctx *context.Context, cancel *context.CancelFunc, timeout time.Duration) {
if *ctx == nil {
*ctx = context.Background()
}
*ctx, *cancel = context.WithTimeout(*ctx, timeout)
}
// SetDeadline creates the given context, if needed, and sets the given deadline.
func SetDeadline(ctx *context.Context, cancel *context.CancelFunc, deadline time.Time) {
if *ctx == nil {
*ctx = context.Background()
}
*ctx, *cancel = context.WithDeadline(*ctx, deadline)
}

View file

@ -0,0 +1,325 @@
/*
Copyright (c) 2021 Red Hat, Inc.
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.
*/
// This file contains the implementation of the server address parser.
package internal
import (
"context"
"fmt"
neturl "net/url"
"strings"
)
// ServerAddress contains a parsed URL and additional information extracted from int, like the
// network (tcp or unix) and the socket name (for Unix sockets).
type ServerAddress struct {
// Text is the original text that was passed to the ParseServerAddress function to create
// this server address.
Text string
// Network is the network that should be used to connect to the server. Possible values are
// `tcp` and `unix`.
Network string
// Protocol is the application protocol used to connect to the server. Possible values are
// `http`, `https` and `h2c`.
Protocol string
// Host is the name of the host used to connect to the server. This will be populated only
// even when using Unix sockets, because clients will need it in order to populate the
// `Host` header.
Host string
// Port is the port number used to connect to the server. This will only be populated when
// using TCP. When using Unix sockets it will be zero.
Port string
// Socket is tha nem of the path of the Unix socket used to connect to the server.
Socket string
// URL is the regular URL calculated from this server address. The scheme will be `http` if
// the protocol is `http` or `h2c` and will be `https` if the protocol is https.
URL *neturl.URL
}
// ParseServerAddress parses the given text as a server address. Server addresses should be URLs
// with this format:
//
// network+protocol://host:port/path?network=...&protocol=...&socket=...
//
// The `network` and `protocol` parts of the scheme are optional.
//
// Valid values for the `network` part of the scheme are `unix` and `tcp`. If not specified the
// default value is `tcp`.
//
// Valid values for the `protocol` part of the scheme are `http`, `https` and `h2c`. If not
// specified the default value is `http`.
//
// The `host` is mandatory even when using Unix sockets, because it is necessary to populate the
// `Host` header.
//
// The `port` part is optional. If not specified it will be 80 for HTTP and H2C and 443 for HTTPS.
//
// When using Unix sockets the `path` part will be used as the name of the Unix socket.
//
// The network protocol and Unix socket can alternatively be specified using the `network`,
// `protocol` and `socket` query parameters. This is useful specially for specifying the Unix
// sockets when the path of the URL has some other meaning. For example, in order to specify
// the OpenID token URL it is usually necessary to include a path, so to use a Unix socket it
// is necessary to put it in the `socket` parameter instead:
//
// unix://my.sso.com/my/token/path?socket=/sockets/my.socket
//
// When the Unix socket is specified in the `socket` query parameter as in the above example
// the URL path will be ignored.
//
// Some examples of valid server addresses:
//
// - http://my.server.com - HTTP on top of TCP.
// - https://my.server.com - HTTPS on top of TCP.
// - unix://my.server.com/sockets/my.socket - HTTP on top Unix socket.
// - unix+https://my.server.com/sockets/my.socket - HTTPS on top of Unix socket.
// - h2c+unix://my.server.com?socket=/sockets/my.socket - H2C on top of Unix.
func ParseServerAddress(ctx context.Context, text string) (result *ServerAddress, err error) {
// Parse the URL:
parsed, err := neturl.Parse(text)
if err != nil {
return
}
query := parsed.Query()
// Extract the network and protocol from the scheme:
networkFromScheme, protocolFromScheme, err := parseScheme(ctx, parsed.Scheme)
if err != nil {
return
}
// Check if the network is also specified with a query parameter. If it is it should not be
// conflicting with the value specified in the scheme.
var network string
networkValues, ok := query["network"]
if ok {
if len(networkValues) != 1 {
err = fmt.Errorf(
"expected exactly one value for the 'network' query parameter "+
"but found %d",
len(networkValues),
)
return
}
networkFromQuery := strings.TrimSpace(strings.ToLower(networkValues[0]))
err = checkNetwork(networkFromQuery)
if err != nil {
return
}
if networkFromScheme != "" && networkFromScheme != networkFromQuery {
err = fmt.Errorf(
"network '%s' from query parameter isn't compatible with "+
"network '%s' from scheme",
networkFromQuery, networkFromScheme,
)
return
}
network = networkFromQuery
} else {
network = networkFromScheme
}
// Check if the protocol is also specified with a query parameter. If it is it should not be
// conflicting with the value specified in the scheme.
var protocol string
protocolValues, ok := query["protocol"]
if ok {
if len(protocolValues) != 1 {
err = fmt.Errorf(
"expected exactly one value for the 'protocol' query parameter "+
"but found %d",
len(protocolValues),
)
return
}
protocolFromQuery := strings.TrimSpace(strings.ToLower(protocolValues[0]))
err = checkProtocol(protocolFromQuery)
if err != nil {
return
}
if protocolFromScheme != "" && protocolFromScheme != protocolFromQuery {
err = fmt.Errorf(
"protocol '%s' from query parameter isn't compatible with "+
"protocol '%s' from scheme",
protocolFromQuery, protocolFromScheme,
)
return
}
protocol = protocolFromQuery
} else {
protocol = protocolFromScheme
}
// Set default values for the network and protocol if needed:
if network == "" {
network = TCPNetwork
}
if protocol == "" {
protocol = HTTPProtocol
}
// Get the host name. Note that the host name is mandatory even when using Unix sockets,
// because it is used to populate the `Host` header.
host := parsed.Hostname()
if host == "" {
err = fmt.Errorf("host name is mandatory, but it is empty")
return
}
// Get the port number:
port := parsed.Port()
if port == "" {
switch protocol {
case HTTPProtocol, H2CProtocol:
port = "80"
case HTTPSProtocol:
port = "443"
}
}
// Get the socket from the `socket` query parameter or from the path:
var socket string
if network == UnixNetwork {
socketValues, ok := query["socket"]
if ok {
if len(socketValues) != 1 {
err = fmt.Errorf(
"expected exactly one value for the 'socket' query "+
"parameter but found %d",
len(socketValues),
)
return
}
socket = socketValues[0]
} else {
socket = parsed.Path
}
if socket == "" {
err = fmt.Errorf(
"expected socket name in the 'socket' query parameter or in " +
"the path but both are empty",
)
return
}
}
// Calculate the URL:
url := &neturl.URL{
Host: host,
}
switch protocol {
case HTTPProtocol, H2CProtocol:
url.Scheme = "http"
if port != "80" {
url.Host = fmt.Sprintf("%s:%s", url.Host, port)
}
case HTTPSProtocol:
url.Scheme = "https"
if port != "443" {
url.Host = fmt.Sprintf("%s:%s", url.Host, port)
}
}
// Create and populate the result:
result = &ServerAddress{
Text: text,
Network: network,
Protocol: protocol,
Host: host,
Port: port,
Socket: socket,
URL: url,
}
return
}
func parseScheme(ctx context.Context, scheme string) (network, protocol string,
err error) {
components := strings.Split(strings.ToLower(scheme), "+")
if len(components) > 2 {
err = fmt.Errorf(
"scheme '%s' should have at most two components separated by '+', "+
"but it has %d",
scheme, len(components),
)
return
}
for _, component := range components {
switch strings.TrimSpace(component) {
case TCPNetwork, UnixNetwork:
network = component
case HTTPProtocol, HTTPSProtocol, H2CProtocol:
protocol = component
default:
err = fmt.Errorf(
"component '%s' of scheme '%s' doesn't correspond to any "+
"supported network or protocol, supported networks "+
"are 'tcp' and 'unix', supported protocols are 'http', "+
"'https' and 'h2c'",
component, scheme,
)
return
}
}
return
}
func checkNetwork(value string) error {
switch value {
case UnixNetwork, TCPNetwork:
return nil
default:
return fmt.Errorf(
"network '%s' isn't valid, valid values are 'unix' and 'tcp'",
value,
)
}
}
func checkProtocol(value string) error {
switch value {
case HTTPProtocol, HTTPSProtocol, H2CProtocol:
return nil
default:
return fmt.Errorf(
"protocol '%s' isn't valid, valid values are 'http', 'https' "+
"and 'h2c'",
value,
)
}
}
// Network names:
const (
UnixNetwork = "unix"
TCPNetwork = "tcp"
)
// Protocol names:
const (
HTTPProtocol = "http"
HTTPSProtocol = "https"
H2CProtocol = "h2c"
)

View file

@ -0,0 +1,32 @@
// +build !windows
/*
Copyright (c) 2021 Red Hat, Inc.
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.
*/
// This file contains the function that returns the trusted CA certificates for operating systems
// other than Windows, where Go knows how to load the system trusted CA store.
package internal
import (
"crypto/x509"
)
// loadSystemCAs loads the trusted CA certifites from the system trusted CA store.
func loadSystemCAs() (pool *x509.CertPool, err error) {
pool, err = x509.SystemCertPool()
return
}

View file

@ -0,0 +1,110 @@
// +build windows
/*
Copyright (c) 2021 Red Hat, Inc.
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.
*/
// This file contains the function that returns the trusted CA certificates for Windows. This is
// needed because currently Go doesn't know how to load the Windows trusted CA store. See the
// following issues for more information:
//
// https://github.com/golang/go/issues/16736
// https://github.com/golang/go/issues/18609
package internal
import (
"crypto/x509"
)
// loadSystemCAs loads the certificates of the CAs that we will trust. Currently this uses a fixed
// set of CA certificates, which is obviusly going to break in the future, but there is not much we
// can do (or know to do) till Go learns to read the Windows CA trust store.
func loadSystemCAs() (pool *x509.CertPool, err error) {
pool = x509.NewCertPool()
pool.AppendCertsFromPEM(ssoCA)
pool.AppendCertsFromPEM(apiCA)
return
}
// This certificate has been obtained with the following command:
//
// $ openssl s_client \
// -connect sso.redhat.com:443 \
// -showcerts
var ssoCA = []byte(`
-----BEGIN CERTIFICATE-----
MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL
MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW
YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY
uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/
LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy
/Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh
cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k
8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB
Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy
dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2
MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j
b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW
gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh
hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg
4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa
2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs
1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1
oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn
8TUoE6smftX3eg==
-----END CERTIFICATE-----
`)
// This certificate has been obtained with the following command:
//
// $ openssl s_client \
// -connect api.openshift.com:443 \
// -showcerts
var apiCA = []byte(`
-----BEGIN CERTIFICATE-----
MIIEZTCCA02gAwIBAgIQQAF1BIMUpMghjISpDBbN3zANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTIwMTAwNzE5MjE0MFoXDTIxMDkyOTE5MjE0MFow
MjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxCzAJBgNVBAMT
AlIzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuwIVKMz2oJTTDxLs
jVWSw/iC8ZmmekKIp10mqrUrucVMsa+Oa/l1yKPXD0eUFFU1V4yeqKI5GfWCPEKp
Tm71O8Mu243AsFzzWTjn7c9p8FoLG77AlCQlh/o3cbMT5xys4Zvv2+Q7RVJFlqnB
U840yFLuta7tj95gcOKlVKu2bQ6XpUA0ayvTvGbrZjR8+muLj1cpmfgwF126cm/7
gcWt0oZYPRfH5wm78Sv3htzB2nFd1EbjzK0lwYi8YGd1ZrPxGPeiXOZT/zqItkel
/xMY6pgJdz+dU/nPAeX1pnAXFK9jpP+Zs5Od3FOnBv5IhR2haa4ldbsTzFID9e1R
oYvbFQIDAQABo4IBaDCCAWQwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8E
BAMCAYYwSwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5p
ZGVudHJ1c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTE
p7Gkeyxx+tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEE
AYLfEwEBATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2Vu
Y3J5cHQub3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0
LmNvbS9EU1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYf
r52LFMLGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0B
AQsFAAOCAQEA2UzgyfWEiDcx27sT4rP8i2tiEmxYt0l+PAK3qB8oYevO4C5z70kH
ejWEHx2taPDY/laBL21/WKZuNTYQHHPD5b1tXgHXbnL7KqC401dk5VvCadTQsvd8
S8MXjohyc9z9/G2948kLjmE6Flh9dDYrVYA9x2O+hEPGOaEOa1eePynBgPayvUfL
qjBstzLhWVQLGAkXXmNs+5ZnPBxzDJOLxhF2JIbeQAcH5H0tZrUlo5ZYyOqA7s9p
O5b85o3AM/OJ+CktFBQtfvBhcJVd9wvlwPsk+uyOy2HI7mNxKKgsBTt375teA2Tw
UdHkhVNcsAKX1H7GNNLOEADksd86wuoXvg==
-----END CERTIFICATE-----
`)

View file

@ -0,0 +1,175 @@
/*
Copyright (c) 2018 Red Hat, Inc.
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.
*/
// This file contains a logger that uses the `glog` package.
package logging
import (
"context"
"fmt"
"os"
"github.com/golang/glog"
)
// GlogLoggerBuilder contains the configuration and logic needed to build a logger that uses the
// glog V mechanism. Don't create instances of this type directly, use the NewGlogLoggerBuilder
// function instead.
type GlogLoggerBuilder struct {
debugV glog.Level
infoV glog.Level
warnV glog.Level
errorV glog.Level
}
// GlogLogger is a logger that uses the glog V mechanism.
type GlogLogger struct {
debugV glog.Level
infoV glog.Level
warnV glog.Level
errorV glog.Level
}
// NewGlogLoggerBuilder creates a builder that uses the glog V mechanism. By default errors,
// warnings and information messages will be written to the log if the level is 0 or greater, and
// debug messages will be written if the level is 1 or greater. This can be changed using the
// ErrorV, WarnV, InfoV and DebugV methods of the builder. For example, to write errors and warnings
// for level 0, information messages for level 1, and debug messages for level 2, you can create the
// logger like this:
//
// logger, err := client.NewGlobLoggerBuilder().
// ErrorV(0).
// WarnV(0).
// InfoV(1).
// DebugV(2).
// Build()
//
// Once the logger is created these settings can't be changed.
func NewGlogLoggerBuilder() *GlogLoggerBuilder {
// Allocate the object:
builder := new(GlogLoggerBuilder)
// Set default values:
builder.debugV = 1
builder.infoV = 0
builder.warnV = 0
builder.errorV = 0
return builder
}
// DebugV sets the V value that will be used for debug messages.
func (b *GlogLoggerBuilder) DebugV(v glog.Level) *GlogLoggerBuilder {
b.debugV = v
return b
}
// InfoV sets the V value that will be used for info messages.
func (b *GlogLoggerBuilder) InfoV(v glog.Level) *GlogLoggerBuilder {
b.infoV = v
return b
}
// WarnV sets the V value that will be used for warn messages.
func (b *GlogLoggerBuilder) WarnV(v glog.Level) *GlogLoggerBuilder {
b.warnV = v
return b
}
// ErrorV sets the V value that will be used for error messages.
func (b *GlogLoggerBuilder) ErrorV(v glog.Level) *GlogLoggerBuilder {
b.errorV = v
return b
}
// Build creates a new logger using the configuration stored in the builder.
func (b *GlogLoggerBuilder) Build() (logger *GlogLogger, err error) {
// Allocate and populate the object:
logger = new(GlogLogger)
logger.debugV = b.debugV
logger.infoV = b.infoV
logger.warnV = b.warnV
logger.errorV = b.errorV
return
}
// DebugEnabled returns true iff the debug level is enabled.
func (l *GlogLogger) DebugEnabled() bool {
return bool(glog.V(l.debugV))
}
// InfoEnabled returns true iff the information level is enabled.
func (l *GlogLogger) InfoEnabled() bool {
return bool(glog.V(l.infoV))
}
// WarnEnabled returns true iff the warning level is enabled.
func (l *GlogLogger) WarnEnabled() bool {
return bool(glog.V(l.warnV))
}
// ErrorEnabled returns true iff the error level is enabled.
func (l *GlogLogger) ErrorEnabled() bool {
return bool(glog.V(l.errorV))
}
// Debug sends to the log a debug message formatted using the fmt.Sprintf function and the given
// format and arguments.
func (l *GlogLogger) Debug(ctx context.Context, format string, args ...interface{}) {
if glog.V(l.debugV) {
msg := fmt.Sprintf(format, args...)
glog.InfoDepth(1, msg)
}
}
// Info sends to the log an information message formatted using the fmt.Sprintf function and the
// given format and arguments.
func (l *GlogLogger) Info(ctx context.Context, format string, args ...interface{}) {
if glog.V(l.infoV) {
msg := fmt.Sprintf(format, args...)
glog.InfoDepth(1, msg)
}
}
// Warn sends to the log a warning message formatted using the fmt.Sprintf function and the given
// format and arguments.
func (l *GlogLogger) Warn(ctx context.Context, format string, args ...interface{}) {
if glog.V(l.warnV) {
msg := fmt.Sprintf(format, args...)
glog.WarningDepth(1, msg)
}
}
// Error sends to the log an error message formatted using the fmt.Sprintf function and the given
// format and arguments.
func (l *GlogLogger) Error(ctx context.Context, format string, args ...interface{}) {
if glog.V(l.errorV) {
msg := fmt.Sprintf(format, args...)
glog.ErrorDepth(1, msg)
}
}
// Fatal sends to the log an error message formatted using the fmt.Sprintf function and the given
// format and arguments. After that it will os.Exit(1)
// This level is always enabled
func (l *GlogLogger) Fatal(ctx context.Context, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
// #nosec G104
glog.ErrorDepth(1, msg)
os.Exit(1)
}

View file

@ -0,0 +1,165 @@
/*
Copyright (c) 2018 Red Hat, Inc.
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.
*/
// This file contains a logger that uses the Go `log` package.
package logging
import (
"context"
"fmt"
"log"
"os"
)
// GoLoggerBuilder contains the configuration and logic needed to build a logger that uses the Go
// `log` package. Don't create instances of this type directly, use the NewGoLoggerBuilder function
// instead.
type GoLoggerBuilder struct {
debugEnabled bool
infoEnabled bool
warnEnabled bool
errorEnabled bool
}
// GoLogger is a logger that uses the Go `log` package.
type GoLogger struct {
debugEnabled bool
infoEnabled bool
warnEnabled bool
errorEnabled bool
}
// NewGoLoggerBuilder creates a builder that knows how to build a logger that uses the Go `log`
// package. By default these loggers will have enabled the information, warning and error levels
func NewGoLoggerBuilder() *GoLoggerBuilder {
// Allocate the object:
builder := new(GoLoggerBuilder)
// Set default values:
builder.debugEnabled = false
builder.infoEnabled = true
builder.warnEnabled = true
builder.errorEnabled = true
return builder
}
// Debug enables or disables the debug level.
func (b *GoLoggerBuilder) Debug(flag bool) *GoLoggerBuilder {
b.debugEnabled = flag
return b
}
// Info enables or disables the information level.
func (b *GoLoggerBuilder) Info(flag bool) *GoLoggerBuilder {
b.infoEnabled = flag
return b
}
// Warn enables or disables the warning level.
func (b *GoLoggerBuilder) Warn(flag bool) *GoLoggerBuilder {
b.warnEnabled = flag
return b
}
// Error enables or disables the error level.
func (b *GoLoggerBuilder) Error(flag bool) *GoLoggerBuilder {
b.errorEnabled = flag
return b
}
// Build creates a new logger using the configuration stored in the builder.
func (b *GoLoggerBuilder) Build() (logger *GoLogger, err error) {
// Allocate and populate the object:
logger = new(GoLogger)
logger.debugEnabled = b.debugEnabled
logger.infoEnabled = b.infoEnabled
logger.warnEnabled = b.warnEnabled
logger.errorEnabled = b.errorEnabled
return
}
// DebugEnabled returns true iff the debug level is enabled.
func (l *GoLogger) DebugEnabled() bool {
return l.debugEnabled
}
// InfoEnabled returns true iff the information level is enabled.
func (l *GoLogger) InfoEnabled() bool {
return l.infoEnabled
}
// WarnEnabled returns true iff the warning level is enabled.
func (l *GoLogger) WarnEnabled() bool {
return l.warnEnabled
}
// ErrorEnabled returns true iff the error level is enabled.
func (l *GoLogger) ErrorEnabled() bool {
return l.errorEnabled
}
// Debug sends to the log a debug message formatted using the fmt.Sprintf function and the given
// format and arguments.
func (l *GoLogger) Debug(ctx context.Context, format string, args ...interface{}) {
if l.debugEnabled {
msg := fmt.Sprintf(format, args...)
// #nosec G104
log.Output(1, msg)
}
}
// Info sends to the log an information message formatted using the fmt.Sprintf function and the
// given format and arguments.
func (l *GoLogger) Info(ctx context.Context, format string, args ...interface{}) {
if l.infoEnabled {
msg := fmt.Sprintf(format, args...)
// #nosec G104
log.Output(1, msg)
}
}
// Warn sends to the log a warning message formatted using the fmt.Sprintf function and the given
// format and arguments.
func (l *GoLogger) Warn(ctx context.Context, format string, args ...interface{}) {
if l.warnEnabled {
msg := fmt.Sprintf(format, args...)
// #nosec G104
log.Output(1, msg)
}
}
// Error sends to the log an error message formatted using the fmt.Sprintf function and the given
// format and arguments.
func (l *GoLogger) Error(ctx context.Context, format string, args ...interface{}) {
if l.errorEnabled {
msg := fmt.Sprintf(format, args...)
// #nosec G104
log.Output(1, msg)
}
}
// Fatal sends to the log an error message formatted using the fmt.Sprintf function and the given
// format and arguments. After that it will os.Exit(1)
// This level is always enabled
func (l *GoLogger) Fatal(ctx context.Context, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
// #nosec G104
log.Output(1, msg)
os.Exit(1)
}

View file

@ -0,0 +1,56 @@
/*
Copyright (c) 2021 Red Hat, Inc.
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.
*/
// This file contains functions useful for printing lists in a format that is easy to read for
// humans in log files.
package logging
import (
"fmt"
"strings"
)
// All generates a human readable representation of the given list of strings, for use in a log
// file. It puts quotes around each item, separates the first items with commas and the last with
// the word 'and'.
func All(items []string) string {
return list(items, "and")
}
// any generates a human readable representation of the given list of strings, for use in a log
// file. It puts quotes around each item, separates the first items with commas and the last with
// the word 'or'.
func Any(items []string) string {
return list(items, "or")
}
func list(items []string, conjunction string) string {
count := len(items)
if count == 0 {
return ""
}
quoted := make([]string, len(items))
for i, item := range items {
quoted[i] = fmt.Sprintf("'%s'", item)
}
if count == 1 {
return quoted[0]
}
head := quoted[0 : count-1]
tail := quoted[count-1]
return fmt.Sprintf("%s %s %s", strings.Join(head, ", "), conjunction, tail)
}

View file

@ -0,0 +1,65 @@
/*
Copyright (c) 2018 Red Hat, Inc.
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.
*/
// This file contains the definition of the logger interface that is used by the client.
package logging
import (
"context"
)
// Logger is the interface that must be implemented by objects that are used for logging by the
// client. By default the client uses a logger based on the `glog` package, but that can be changed
// using the `Logger` method of the builder.
//
// Note that the context is optional in most of the methods of the SDK, so implementations of this
// interface must accept and handle smoothly calls to the Debug, Info, Warn and Error methods where
// the ctx parameter is nil.
type Logger interface {
// DebugEnabled returns true if the debug level is enabled.
DebugEnabled() bool
// InfoEnabled returns true if the information level is enabled.
InfoEnabled() bool
// WarnEnabled returns true if the warning level is enabled.
WarnEnabled() bool
// ErrorEnabled returns true if the error level is enabled.
ErrorEnabled() bool
// Debug sends to the log a debug message formatted using the fmt.Sprintf function and the
// given format and arguments.
Debug(ctx context.Context, format string, args ...interface{})
// Info sends to the log an information message formatted using the fmt.Sprintf function and
// the given format and arguments.
Info(ctx context.Context, format string, args ...interface{})
// Warn sends to the log a warning message formatted using the fmt.Sprintf function and the
// given format and arguments.
Warn(ctx context.Context, format string, args ...interface{})
// Error sends to the log an error message formatted using the fmt.Sprintf function and the
// given format and arguments.
Error(ctx context.Context, format string, args ...interface{})
// Fatal sends to the log an error message formatted using the fmt.Sprintf function and the
// given format and arguments; and then executes an os.Exit(1)
// Fatal level is always enabled
Fatal(ctx context.Context, format string, args ...interface{})
}

View file

@ -0,0 +1,175 @@
/*
Copyright (c) 2018 Red Hat, Inc.
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.
*/
// This file contains a logger that uses the standard output and error streams, or custom writers.
package logging
import (
"context"
"fmt"
"io"
"os"
)
// StdLoggerBuilder contains the configuration and logic needed to build a logger that uses the
// standard output and error streams, or custom writers.
type StdLoggerBuilder struct {
debugEnabled bool
infoEnabled bool
warnEnabled bool
errorEnabled bool
outStream io.Writer
errStream io.Writer
}
// StdLogger is a logger that uses the standard output and error streams, or custom writers.
type StdLogger struct {
debugEnabled bool
infoEnabled bool
warnEnabled bool
errorEnabled bool
outStream io.Writer
errStream io.Writer
}
// NewStdLoggerBuilder creates a builder that knows how to build a logger that uses the standard
// output and error streams, or custom writers. By default these loggers will have enabled the
// information, warning and error levels
func NewStdLoggerBuilder() *StdLoggerBuilder {
// Allocate the object:
builder := new(StdLoggerBuilder)
// Set default values:
builder.debugEnabled = false
builder.infoEnabled = true
builder.warnEnabled = true
builder.errorEnabled = true
return builder
}
// Streams sets the standard output and error streams to use. If not used then the logger will use
// os.Stdout and os.Stderr.
func (b *StdLoggerBuilder) Streams(out io.Writer, err io.Writer) *StdLoggerBuilder {
b.outStream = out
b.errStream = err
return b
}
// Debug enables or disables the debug level.
func (b *StdLoggerBuilder) Debug(flag bool) *StdLoggerBuilder {
b.debugEnabled = flag
return b
}
// Info enables or disables the information level.
func (b *StdLoggerBuilder) Info(flag bool) *StdLoggerBuilder {
b.infoEnabled = flag
return b
}
// Warn enables or disables the warning level.
func (b *StdLoggerBuilder) Warn(flag bool) *StdLoggerBuilder {
b.warnEnabled = flag
return b
}
// Error enables or disables the error level.
func (b *StdLoggerBuilder) Error(flag bool) *StdLoggerBuilder {
b.errorEnabled = flag
return b
}
// Build creates a new logger using the configuration stored in the builder.
func (b *StdLoggerBuilder) Build() (logger *StdLogger, err error) {
// Allocate and populate the object:
logger = new(StdLogger)
logger.debugEnabled = b.debugEnabled
logger.infoEnabled = b.infoEnabled
logger.warnEnabled = b.warnEnabled
logger.errorEnabled = b.errorEnabled
logger.outStream = b.outStream
logger.errStream = b.errStream
if logger.outStream == nil {
logger.outStream = os.Stdout
}
if logger.errStream == nil {
logger.errStream = os.Stderr
}
return
}
// DebugEnabled returns true iff the debug level is enabled.
func (l *StdLogger) DebugEnabled() bool {
return l.debugEnabled
}
// InfoEnabled returns true iff the information level is enabled.
func (l *StdLogger) InfoEnabled() bool {
return l.infoEnabled
}
// WarnEnabled returns true iff the warning level is enabled.
func (l *StdLogger) WarnEnabled() bool {
return l.warnEnabled
}
// ErrorEnabled returns true iff the error level is enabled.
func (l *StdLogger) ErrorEnabled() bool {
return l.errorEnabled
}
// Debug sends to the log a debug message formatted using the fmt.Sprintf function and the given
// format and arguments.
func (l *StdLogger) Debug(ctx context.Context, format string, args ...interface{}) {
if l.debugEnabled {
fmt.Fprintf(l.outStream, format+"\n", args...)
}
}
// Info sends to the log an information message formatted using the fmt.Sprintf function and the
// given format and arguments.
func (l *StdLogger) Info(ctx context.Context, format string, args ...interface{}) {
if l.infoEnabled {
fmt.Fprintf(l.outStream, format+"\n", args...)
}
}
// Warn sends to the log a warning message formatted using the fmt.Sprintf function and the given
// format and arguments.
func (l *StdLogger) Warn(ctx context.Context, format string, args ...interface{}) {
if l.warnEnabled {
fmt.Fprintf(l.outStream, format+"\n", args...)
}
}
// Error sends to the log an error message formatted using the fmt.Sprintf function and the given
// format and arguments.
func (l *StdLogger) Error(ctx context.Context, format string, args ...interface{}) {
if l.errorEnabled {
fmt.Fprintf(l.errStream, format+"\n", args...)
}
}
// Fatal sends to the log an error message formatted using the fmt.Sprintf function and the given
// format and arguments. After that it will os.Exit(1)
// This level is always enabled
func (l *StdLogger) Fatal(ctx context.Context, format string, args ...interface{}) {
fmt.Fprintf(l.errStream, format+"\n", args...)
os.Exit(1)
}