Commit d75bd8bf authored by Nikhil Jindal's avatar Nikhil Jindal

Merge pull request #7101 from liggitt/service_account

ServiceAccounts
parents 4e023e9f 92bd58ed
...@@ -90,6 +90,11 @@ ...@@ -90,6 +90,11 @@
"Rev": "b5c0891944c2f150ccc9d02aecf51b76c14c2948" "Rev": "b5c0891944c2f150ccc9d02aecf51b76c14c2948"
}, },
{ {
"ImportPath": "github.com/dgrijalva/jwt-go",
"Comment": "v2.2.0-23-g5ca8014",
"Rev": "5ca80149b9d3f8b863af0e2bb6742e608603bd99"
},
{
"ImportPath": "github.com/docker/docker/pkg/archive", "ImportPath": "github.com/docker/docker/pkg/archive",
"Comment": "v1.4.1-1714-ged66853", "Comment": "v1.4.1-1714-ged66853",
"Rev": "ed6685369740035b0af9675bf9add52d0af7657b" "Rev": "ed6685369740035b0af9675bf9add52d0af7657b"
......
Copyright (c) 2012 Dave Grijalva
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-jones-json-web-token.html)
**NOTICE:** A vulnerability in JWT was [recently published](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). As this library doesn't force users to validate the `alg` is what they expected, it's possible your usage is effected. There will be an update soon to remedy this, and it will likey require backwards-incompatible changes to the API. In the short term, please make sure your implementation verifies the `alg` is what you expect.
## What the heck is a JWT?
In short, it's a signed JSON object that does something useful (for example, authentication). It's commonly used for `Bearer` tokens in Oauth 2. A token is made of three parts, separated by `.`'s. The first two parts are JSON objects, that have been [base64url](http://tools.ietf.org/html/rfc4648) encoded. The last part is the signature, encoded the same way.
The first part is called the header. It contains the necessary information for verifying the last part, the signature. For example, which encryption method was used for signing and what key was used.
The part in the middle is the interesting bit. It's called the Claims and contains the actual stuff you care about. Refer to [the RFC](http://self-issued.info/docs/draft-jones-json-web-token.html) for information about reserved keys and the proper way to add your own.
## What's in the box?
This library supports the parsing and verification as well as the generation and signing of JWTs. Current supported signing algorithms are RSA256 and HMAC SHA256, though hooks are present for adding your own.
## Parse and Verify
Parsing and verifying tokens is pretty straight forward. You pass in the token and a function for looking up the key. This is done as a callback since you may need to parse the token to find out what signing method and key was used.
```go
token, err := jwt.Parse(myToken, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return myLookupKey(token.Header["kid"])
})
if err == nil && token.Valid {
deliverGoodness("!")
} else {
deliverUtterRejection(":(")
}
```
## Create a token
```go
// Create the token
token := jwt.New(jwt.SigningMethodHS256)
// Set some claims
token.Claims["foo"] = "bar"
token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
// Sign and get the complete encoded token as a string
tokenString, err := token.SignedString(mySigningKey)
```
## Project Status & Versioning
This library is considered production ready. Feedback and feature requests are appreciated. The API should be considered stable. There should be very few backwards-incompatible changes outside of major version updates (and only with good reason).
This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases).
While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v2`. It will do the right thing WRT semantic versioning.
## More
Documentation can be found [on godoc.org](http://godoc.org/github.com/dgrijalva/jwt-go).
The command line utility included in this project (cmd/jwt) provides a straightforward example of token creation and parsing as well as a useful tool for debugging your own integration. For a more http centric example, see [this gist](https://gist.github.com/cryptix/45c33ecf0ae54828e63b).
## `jwt-go` Version History
#### 2.2.0
* Gracefully handle a `nil` `Keyfunc` being passed to `Parse`. Result will now be the parsed token and an error, instead of a panic.
#### 2.1.0
Backwards compatible API change that was missed in 2.0.0.
* The `SignedString` method on `Token` now takes `interface{}` instead of `[]byte`
#### 2.0.0
There were two major reasons for breaking backwards compatibility with this update. The first was a refactor required to expand the width of the RSA and HMAC-SHA signing implementations. There will likely be no required code changes to support this change.
The second update, while unfortunately requiring a small change in integration, is required to open up this library to other signing methods. Not all keys used for all signing methods have a single standard on-disk representation. Requiring `[]byte` as the type for all keys proved too limiting. Additionally, this implementation allows for pre-parsed tokens to be reused, which might matter in an application that parses a high volume of tokens with a small set of keys. Backwards compatibilty has been maintained for passing `[]byte` to the RSA signing methods, but they will also accept `*rsa.PublicKey` and `*rsa.PrivateKey`.
It is likely the only integration change required here will be to change `func(t *jwt.Token) ([]byte, error)` to `func(t *jwt.Token) (interface{}, error)` when calling `Parse`.
* **Compatibility Breaking Changes**
* `SigningMethodHS256` is now `*SigningMethodHMAC` instead of `type struct`
* `SigningMethodRS256` is now `*SigningMethodRSA` instead of `type struct`
* `KeyFunc` now returns `interface{}` instead of `[]byte`
* `SigningMethod.Sign` now takes `interface{}` instead of `[]byte` for the key
* `SigningMethod.Verify` now takes `interface{}` instead of `[]byte` for the key
* Renamed type `SigningMethodHS256` to `SigningMethodHMAC`. Specific sizes are now just instances of this type.
* Added public package global `SigningMethodHS256`
* Added public package global `SigningMethodHS384`
* Added public package global `SigningMethodHS512`
* Renamed type `SigningMethodRS256` to `SigningMethodRSA`. Specific sizes are now just instances of this type.
* Added public package global `SigningMethodRS256`
* Added public package global `SigningMethodRS384`
* Added public package global `SigningMethodRS512`
* Moved sample private key for HMAC tests from an inline value to a file on disk. Value is unchanged.
* Refactored the RSA implementation to be easier to read
* Exposed helper methods `ParseRSAPrivateKeyFromPEM` and `ParseRSAPublicKeyFromPEM`
#### 1.0.2
* Fixed bug in parsing public keys from certificates
* Added more tests around the parsing of keys for RS256
* Code refactoring in RS256 implementation. No functional changes
#### 1.0.1
* Fixed panic if RS256 signing method was passed an invalid key
#### 1.0.0
* First versioned release
* API stabilized
* Supports creating, signing, parsing, and validating JWT tokens
* Supports RS256 and HS256 signing methods
\ No newline at end of file
// A useful example app. You can use this to debug your tokens on the command line.
// This is also a great place to look at how you might use this library.
//
// Example usage:
// The following will create and sign a token, then verify it and output the original claims.
// echo {\"foo\":\"bar\"} | bin/jwt -key test/sample_key -alg RS256 -sign - | bin/jwt -key test/sample_key.pub -verify -
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"regexp"
"github.com/dgrijalva/jwt-go"
)
var (
// Options
flagAlg = flag.String("alg", "", "signing algorithm identifier")
flagKey = flag.String("key", "", "path to key file or '-' to read from stdin")
flagCompact = flag.Bool("compact", false, "output compact JSON")
flagDebug = flag.Bool("debug", false, "print out all kinds of debug data")
// Modes - exactly one of these is required
flagSign = flag.String("sign", "", "path to claims object to sign or '-' to read from stdin")
flagVerify = flag.String("verify", "", "path to JWT token to verify or '-' to read from stdin")
)
func main() {
// Usage message if you ask for -help or if you mess up inputs.
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
fmt.Fprintf(os.Stderr, " One of the following flags is required: sign, verify\n")
flag.PrintDefaults()
}
// Parse command line options
flag.Parse()
// Do the thing. If something goes wrong, print error to stderr
// and exit with a non-zero status code
if err := start(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
// Figure out which thing to do and then do that
func start() error {
if *flagSign != "" {
return signToken()
} else if *flagVerify != "" {
return verifyToken()
} else {
flag.Usage()
return fmt.Errorf("None of the required flags are present. What do you want me to do?")
}
}
// Helper func: Read input from specified file or stdin
func loadData(p string) ([]byte, error) {
if p == "" {
return nil, fmt.Errorf("No path specified")
}
var rdr io.Reader
if p == "-" {
rdr = os.Stdin
} else {
if f, err := os.Open(p); err == nil {
rdr = f
defer f.Close()
} else {
return nil, err
}
}
return ioutil.ReadAll(rdr)
}
// Print a json object in accordance with the prophecy (or the command line options)
func printJSON(j interface{}) error {
var out []byte
var err error
if *flagCompact == false {
out, err = json.MarshalIndent(j, "", " ")
} else {
out, err = json.Marshal(j)
}
if err == nil {
fmt.Println(string(out))
}
return err
}
// Verify a token and output the claims. This is a great example
// of how to verify and view a token.
func verifyToken() error {
// get the token
tokData, err := loadData(*flagVerify)
if err != nil {
return fmt.Errorf("Couldn't read token: %v", err)
}
// trim possible whitespace from token
tokData = regexp.MustCompile(`\s*$`).ReplaceAll(tokData, []byte{})
if *flagDebug {
fmt.Fprintf(os.Stderr, "Token len: %v bytes\n", len(tokData))
}
// Parse the token. Load the key from command line option
token, err := jwt.Parse(string(tokData), func(t *jwt.Token) (interface{}, error) {
return loadData(*flagKey)
})
// Print some debug data
if *flagDebug && token != nil {
fmt.Fprintf(os.Stderr, "Header:\n%v\n", token.Header)
fmt.Fprintf(os.Stderr, "Claims:\n%v\n", token.Claims)
}
// Print an error if we can't parse for some reason
if err != nil {
return fmt.Errorf("Couldn't parse token: %v", err)
}
// Is token invalid?
if !token.Valid {
return fmt.Errorf("Token is invalid")
}
// Print the token details
if err := printJSON(token.Claims); err != nil {
return fmt.Errorf("Failed to output claims: %v", err)
}
return nil
}
// Create, sign, and output a token. This is a great, simple example of
// how to use this library to create and sign a token.
func signToken() error {
// get the token data from command line arguments
tokData, err := loadData(*flagSign)
if err != nil {
return fmt.Errorf("Couldn't read token: %v", err)
} else if *flagDebug {
fmt.Fprintf(os.Stderr, "Token: %v bytes", len(tokData))
}
// parse the JSON of the claims
var claims map[string]interface{}
if err := json.Unmarshal(tokData, &claims); err != nil {
return fmt.Errorf("Couldn't parse claims JSON: %v", err)
}
// get the key
keyData, err := loadData(*flagKey)
if err != nil {
return fmt.Errorf("Couldn't read key: %v", err)
}
// get the signing alg
alg := jwt.GetSigningMethod(*flagAlg)
if alg == nil {
return fmt.Errorf("Couldn't find signing method: %v", *flagAlg)
}
// create a new token
token := jwt.New(alg)
token.Claims = claims
if out, err := token.SignedString(keyData); err == nil {
fmt.Println(out)
} else {
return fmt.Errorf("Error signing token: %v", err)
}
return nil
}
// Package jwt is a Go implementation of JSON Web Tokens: http://self-issued.info/docs/draft-jones-json-web-token.html
//
// See README.md for more info.
package jwt
package jwt
import (
"errors"
)
// Error constants
var (
ErrInvalidKey = errors.New("key is invalid or of invalid type")
ErrHashUnavailable = errors.New("the requested hash function is unavailable")
ErrNoTokenInRequest = errors.New("no token present in request")
)
// The errors that might occur when parsing and validating a token
const (
ValidationErrorMalformed uint32 = 1 << iota // Token is malformed
ValidationErrorUnverifiable // Token could not be verified because of signing problems
ValidationErrorSignatureInvalid // Signature validation failed
ValidationErrorExpired // Exp validation failed
ValidationErrorNotValidYet // NBF validation failed
)
// The error from Parse if token is not valid
type ValidationError struct {
err string
Errors uint32 // bitfield. see ValidationError... constants
}
// Validation error is an error type
func (e ValidationError) Error() string {
if e.err == "" {
return "token is invalid"
}
return e.err
}
// No errors
func (e *ValidationError) valid() bool {
if e.Errors > 0 {
return false
}
return true
}
package jwt_test
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"time"
)
func ExampleParse(myToken string, myLookupKey func(interface{}) (interface{}, error)) {
token, err := jwt.Parse(myToken, func(token *jwt.Token) (interface{}, error) {
return myLookupKey(token.Header["kid"])
})
if err == nil && token.Valid {
fmt.Println("Your token is valid. I like your style.")
} else {
fmt.Println("This token is terrible! I cannot accept this.")
}
}
func ExampleNew(mySigningKey []byte) (string, error) {
// Create the token
token := jwt.New(jwt.SigningMethodHS256)
// Set some claims
token.Claims["foo"] = "bar"
token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
// Sign and get the complete encoded token as a string
tokenString, err := token.SignedString(mySigningKey)
return tokenString, err
}
func ExampleParse_errorChecking(myToken string, myLookupKey func(interface{}) (interface{}, error)) {
token, err := jwt.Parse(myToken, func(token *jwt.Token) (interface{}, error) {
return myLookupKey(token.Header["kid"])
})
if token.Valid {
fmt.Println("You look nice today")
} else if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
fmt.Println("That's not even a token")
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
// Token is either expired or not active yet
fmt.Println("Timing is everything")
} else {
fmt.Println("Couldn't handle this token:", err)
}
} else {
fmt.Println("Couldn't handle this token:", err)
}
}
package jwt
import (
"crypto"
"crypto/hmac"
"errors"
)
// Implements the HMAC-SHA family of signing methods signing methods
type SigningMethodHMAC struct {
Name string
Hash crypto.Hash
}
// Specific instances for HS256 and company
var (
SigningMethodHS256 *SigningMethodHMAC
SigningMethodHS384 *SigningMethodHMAC
SigningMethodHS512 *SigningMethodHMAC
ErrSignatureInvalid = errors.New("signature is invalid")
)
func init() {
// HS256
SigningMethodHS256 = &SigningMethodHMAC{"HS256", crypto.SHA256}
RegisterSigningMethod(SigningMethodHS256.Alg(), func() SigningMethod {
return SigningMethodHS256
})
// HS384
SigningMethodHS384 = &SigningMethodHMAC{"HS384", crypto.SHA384}
RegisterSigningMethod(SigningMethodHS384.Alg(), func() SigningMethod {
return SigningMethodHS384
})
// HS512
SigningMethodHS512 = &SigningMethodHMAC{"HS512", crypto.SHA512}
RegisterSigningMethod(SigningMethodHS512.Alg(), func() SigningMethod {
return SigningMethodHS512
})
}
func (m *SigningMethodHMAC) Alg() string {
return m.Name
}
func (m *SigningMethodHMAC) Verify(signingString, signature string, key interface{}) error {
if keyBytes, ok := key.([]byte); ok {
var sig []byte
var err error
if sig, err = DecodeSegment(signature); err == nil {
if !m.Hash.Available() {
return ErrHashUnavailable
}
hasher := hmac.New(m.Hash.New, keyBytes)
hasher.Write([]byte(signingString))
if !hmac.Equal(sig, hasher.Sum(nil)) {
err = ErrSignatureInvalid
}
}
return err
}
return ErrInvalidKey
}
// Implements the Sign method from SigningMethod for this signing method.
// Key must be []byte
func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) {
if keyBytes, ok := key.([]byte); ok {
if !m.Hash.Available() {
return "", ErrHashUnavailable
}
hasher := hmac.New(m.Hash.New, keyBytes)
hasher.Write([]byte(signingString))
return EncodeSegment(hasher.Sum(nil)), nil
}
return "", ErrInvalidKey
}
package jwt_test
import (
"github.com/dgrijalva/jwt-go"
"io/ioutil"
"strings"
"testing"
)
var hmacTestData = []struct {
name string
tokenString string
alg string
claims map[string]interface{}
valid bool
}{
{
"web sample",
"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
"HS256",
map[string]interface{}{"iss": "joe", "exp": 1300819380, "http://example.com/is_root": true},
true,
},
{
"HS384",
"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJleHAiOjEuMzAwODE5MzhlKzA5LCJodHRwOi8vZXhhbXBsZS5jb20vaXNfcm9vdCI6dHJ1ZSwiaXNzIjoiam9lIn0.KWZEuOD5lbBxZ34g7F-SlVLAQ_r5KApWNWlZIIMyQVz5Zs58a7XdNzj5_0EcNoOy",
"HS384",
map[string]interface{}{"iss": "joe", "exp": 1300819380, "http://example.com/is_root": true},
true,
},
{
"HS512",
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEuMzAwODE5MzhlKzA5LCJodHRwOi8vZXhhbXBsZS5jb20vaXNfcm9vdCI6dHJ1ZSwiaXNzIjoiam9lIn0.CN7YijRX6Aw1n2jyI2Id1w90ja-DEMYiWixhYCyHnrZ1VfJRaFQz1bEbjjA5Fn4CLYaUG432dEYmSbS4Saokmw",
"HS512",
map[string]interface{}{"iss": "joe", "exp": 1300819380, "http://example.com/is_root": true},
true,
},
{
"web sample: invalid",
"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXo",
"HS256",
map[string]interface{}{"iss": "joe", "exp": 1300819380, "http://example.com/is_root": true},
false,
},
}
// Sample data from http://tools.ietf.org/html/draft-jones-json-web-signature-04#appendix-A.1
var hmacTestKey, _ = ioutil.ReadFile("test/hmacTestKey")
func TestHMACVerify(t *testing.T) {
for _, data := range hmacTestData {
parts := strings.Split(data.tokenString, ".")
method := jwt.GetSigningMethod(data.alg)
err := method.Verify(strings.Join(parts[0:2], "."), parts[2], hmacTestKey)
if data.valid && err != nil {
t.Errorf("[%v] Error while verifying key: %v", data.name, err)
}
if !data.valid && err == nil {
t.Errorf("[%v] Invalid key passed validation", data.name)
}
}
}
func TestHMACSign(t *testing.T) {
for _, data := range hmacTestData {
if data.valid {
parts := strings.Split(data.tokenString, ".")
method := jwt.GetSigningMethod(data.alg)
sig, err := method.Sign(strings.Join(parts[0:2], "."), hmacTestKey)
if err != nil {
t.Errorf("[%v] Error signing token: %v", data.name, err)
}
if sig != parts[2] {
t.Errorf("[%v] Incorrect signature.\nwas:\n%v\nexpecting:\n%v", data.name, sig, parts[2])
}
}
}
}
func BenchmarkHS256Signing(b *testing.B) {
benchmarkSigning(b, jwt.SigningMethodHS256, hmacTestKey)
}
func BenchmarkHS384Signing(b *testing.B) {
benchmarkSigning(b, jwt.SigningMethodHS384, hmacTestKey)
}
func BenchmarkHS512Signing(b *testing.B) {
benchmarkSigning(b, jwt.SigningMethodHS512, hmacTestKey)
}
package jwt
import (
"encoding/base64"
"encoding/json"
"net/http"
"strings"
"time"
)
// TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time).
// You can override it to use another time value. This is useful for testing or if your
// server uses a different time zone than your tokens.
var TimeFunc = time.Now
// Parse methods use this callback function to supply
// the key for verification. The function receives the parsed,
// but unverified Token. This allows you to use propries in the
// Header of the token (such as `kid`) to identify which key to use.
type Keyfunc func(*Token) (interface{}, error)
// A JWT Token. Different fields will be used depending on whether you're
// creating or parsing/verifying a token.
type Token struct {
Raw string // The raw token. Populated when you Parse a token
Method SigningMethod // The signing method used or to be used
Header map[string]interface{} // The first segment of the token
Claims map[string]interface{} // The second segment of the token
Signature string // The third segment of the token. Populated when you Parse a token
Valid bool // Is the token valid? Populated when you Parse/Verify a token
}
// Create a new Token. Takes a signing method
func New(method SigningMethod) *Token {
return &Token{
Header: map[string]interface{}{
"typ": "JWT",
"alg": method.Alg(),
},
Claims: make(map[string]interface{}),
Method: method,
}
}
// Get the complete, signed token
func (t *Token) SignedString(key interface{}) (string, error) {
var sig, sstr string
var err error
if sstr, err = t.SigningString(); err != nil {
return "", err
}
if sig, err = t.Method.Sign(sstr, key); err != nil {
return "", err
}
return strings.Join([]string{sstr, sig}, "."), nil
}
// Generate the signing string. This is the
// most expensive part of the whole deal. Unless you
// need this for something special, just go straight for
// the SignedString.
func (t *Token) SigningString() (string, error) {
var err error
parts := make([]string, 2)
for i, _ := range parts {
var source map[string]interface{}
if i == 0 {
source = t.Header
} else {
source = t.Claims
}
var jsonValue []byte
if jsonValue, err = json.Marshal(source); err != nil {
return "", err
}
parts[i] = EncodeSegment(jsonValue)
}
return strings.Join(parts, "."), nil
}
// Parse, validate, and return a token.
// keyFunc will receive the parsed token and should return the key for validating.
// If everything is kosher, err will be nil
func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return nil, &ValidationError{err: "token contains an invalid number of segments", Errors: ValidationErrorMalformed}
}
var err error
token := &Token{Raw: tokenString}
// parse Header
var headerBytes []byte
if headerBytes, err = DecodeSegment(parts[0]); err != nil {
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed}
}
if err = json.Unmarshal(headerBytes, &token.Header); err != nil {
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed}
}
// parse Claims
var claimBytes []byte
if claimBytes, err = DecodeSegment(parts[1]); err != nil {
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed}
}
if err = json.Unmarshal(claimBytes, &token.Claims); err != nil {
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorMalformed}
}
// Lookup signature method
if method, ok := token.Header["alg"].(string); ok {
if token.Method = GetSigningMethod(method); token.Method == nil {
return token, &ValidationError{err: "signing method (alg) is unavailable.", Errors: ValidationErrorUnverifiable}
}
} else {
return token, &ValidationError{err: "signing method (alg) is unspecified.", Errors: ValidationErrorUnverifiable}
}
// Lookup key
var key interface{}
if keyFunc == nil {
// keyFunc was not provided. short circuiting validation
return token, &ValidationError{err: "no Keyfunc was provided.", Errors: ValidationErrorUnverifiable}
}
if key, err = keyFunc(token); err != nil {
// keyFunc returned an error
return token, &ValidationError{err: err.Error(), Errors: ValidationErrorUnverifiable}
}
// Check expiration times
vErr := &ValidationError{}
now := TimeFunc().Unix()
if exp, ok := token.Claims["exp"].(float64); ok {
if now > int64(exp) {
vErr.err = "token is expired"
vErr.Errors |= ValidationErrorExpired
}
}
if nbf, ok := token.Claims["nbf"].(float64); ok {
if now < int64(nbf) {
vErr.err = "token is not valid yet"
vErr.Errors |= ValidationErrorNotValidYet
}
}
// Perform validation
if err = token.Method.Verify(strings.Join(parts[0:2], "."), parts[2], key); err != nil {
vErr.err = err.Error()
vErr.Errors |= ValidationErrorSignatureInvalid
}
if vErr.valid() {
token.Valid = true
return token, nil
}
return token, vErr
}
// Try to find the token in an http.Request.
// This method will call ParseMultipartForm if there's no token in the header.
// Currently, it looks in the Authorization header as well as
// looking for an 'access_token' request parameter in req.Form.
func ParseFromRequest(req *http.Request, keyFunc Keyfunc) (token *Token, err error) {
// Look for an Authorization header
if ah := req.Header.Get("Authorization"); ah != "" {
// Should be a bearer token
if len(ah) > 6 && strings.ToUpper(ah[0:6]) == "BEARER" {
return Parse(ah[7:], keyFunc)
}
}
// Look for "access_token" parameter
req.ParseMultipartForm(10e6)
if tokStr := req.Form.Get("access_token"); tokStr != "" {
return Parse(tokStr, keyFunc)
}
return nil, ErrNoTokenInRequest
}
// Encode JWT specific base64url encoding with padding stripped
func EncodeSegment(seg []byte) string {
return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=")
}
// Decode JWT specific base64url encoding with padding stripped
func DecodeSegment(seg string) ([]byte, error) {
if l := len(seg) % 4; l > 0 {
seg += strings.Repeat("=", 4-l)
}
return base64.URLEncoding.DecodeString(seg)
}
package jwt_test
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"io/ioutil"
"net/http"
"reflect"
"testing"
"time"
)
var (
jwtTestDefaultKey []byte
defaultKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return jwtTestDefaultKey, nil }
emptyKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, nil }
errorKeyFunc jwt.Keyfunc = func(t *jwt.Token) (interface{}, error) { return nil, fmt.Errorf("error loading key") }
nilKeyFunc jwt.Keyfunc = nil
)
var jwtTestData = []struct {
name string
tokenString string
keyfunc jwt.Keyfunc
claims map[string]interface{}
valid bool
errors uint32
}{
{
"basic",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
defaultKeyFunc,
map[string]interface{}{"foo": "bar"},
true,
0,
},
{
"basic expired",
"", // autogen
defaultKeyFunc,
map[string]interface{}{"foo": "bar", "exp": float64(time.Now().Unix() - 100)},
false,
jwt.ValidationErrorExpired,
},
{
"basic nbf",
"", // autogen
defaultKeyFunc,
map[string]interface{}{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)},
false,
jwt.ValidationErrorNotValidYet,
},
{
"expired and nbf",
"", // autogen
defaultKeyFunc,
map[string]interface{}{"foo": "bar", "nbf": float64(time.Now().Unix() + 100), "exp": float64(time.Now().Unix() - 100)},
false,
jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired,
},
{
"basic invalid",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.EhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
defaultKeyFunc,
map[string]interface{}{"foo": "bar"},
false,
jwt.ValidationErrorSignatureInvalid,
},
{
"basic nokeyfunc",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
nilKeyFunc,
map[string]interface{}{"foo": "bar"},
false,
jwt.ValidationErrorUnverifiable,
},
{
"basic nokey",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
emptyKeyFunc,
map[string]interface{}{"foo": "bar"},
false,
jwt.ValidationErrorSignatureInvalid,
},
{
"basic errorkey",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
errorKeyFunc,
map[string]interface{}{"foo": "bar"},
false,
jwt.ValidationErrorUnverifiable,
},
}
func init() {
var e error
if jwtTestDefaultKey, e = ioutil.ReadFile("test/sample_key.pub"); e != nil {
panic(e)
}
}
func makeSample(c map[string]interface{}) string {
key, e := ioutil.ReadFile("test/sample_key")
if e != nil {
panic(e.Error())
}
token := jwt.New(jwt.SigningMethodRS256)
token.Claims = c
s, e := token.SignedString(key)
if e != nil {
panic(e.Error())
}
return s
}
func TestJWT(t *testing.T) {
for _, data := range jwtTestData {
if data.tokenString == "" {
data.tokenString = makeSample(data.claims)
}
token, err := jwt.Parse(data.tokenString, data.keyfunc)
if !reflect.DeepEqual(data.claims, token.Claims) {
t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims)
}
if data.valid && err != nil {
t.Errorf("[%v] Error while verifying token: %T:%v", data.name, err, err)
}
if !data.valid && err == nil {
t.Errorf("[%v] Invalid token passed validation", data.name)
}
if data.errors != 0 {
if err == nil {
t.Errorf("[%v] Expecting error. Didn't get one.", data.name)
} else {
// compare the bitfield part of the error
if err.(*jwt.ValidationError).Errors != data.errors {
t.Errorf("[%v] Errors don't match expectation", data.name)
}
}
}
}
}
func TestParseRequest(t *testing.T) {
// Bearer token request
for _, data := range jwtTestData {
if data.tokenString == "" {
data.tokenString = makeSample(data.claims)
}
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", data.tokenString))
token, err := jwt.ParseFromRequest(r, data.keyfunc)
if token == nil {
t.Errorf("[%v] Token was not found: %v", data.name, err)
continue
}
if !reflect.DeepEqual(data.claims, token.Claims) {
t.Errorf("[%v] Claims mismatch. Expecting: %v Got: %v", data.name, data.claims, token.Claims)
}
if data.valid && err != nil {
t.Errorf("[%v] Error while verifying token: %v", data.name, err)
}
if !data.valid && err == nil {
t.Errorf("[%v] Invalid token passed validation", data.name)
}
}
}
// Helper method for benchmarking various methods
func benchmarkSigning(b *testing.B, method jwt.SigningMethod, key interface{}) {
t := jwt.New(method)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
if _, err := t.SignedString(key); err != nil {
b.Fatal(err)
}
}
})
}
package jwt
import (
"crypto"
"crypto/rand"
"crypto/rsa"
)
// Implements the RSA family of signing methods signing methods
type SigningMethodRSA struct {
Name string
Hash crypto.Hash
}
// Specific instances for RS256 and company
var (
SigningMethodRS256 *SigningMethodRSA
SigningMethodRS384 *SigningMethodRSA
SigningMethodRS512 *SigningMethodRSA
)
func init() {
// RS256
SigningMethodRS256 = &SigningMethodRSA{"RS256", crypto.SHA256}
RegisterSigningMethod(SigningMethodRS256.Alg(), func() SigningMethod {
return SigningMethodRS256
})
// RS384
SigningMethodRS384 = &SigningMethodRSA{"RS384", crypto.SHA384}
RegisterSigningMethod(SigningMethodRS384.Alg(), func() SigningMethod {
return SigningMethodRS384
})
// RS512
SigningMethodRS512 = &SigningMethodRSA{"RS512", crypto.SHA512}
RegisterSigningMethod(SigningMethodRS512.Alg(), func() SigningMethod {
return SigningMethodRS512
})
}
func (m *SigningMethodRSA) Alg() string {
return m.Name
}
// Implements the Verify method from SigningMethod
// For this signing method, must be either a PEM encoded PKCS1 or PKCS8 RSA public key as
// []byte, or an rsa.PublicKey structure.
func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error {
var err error
// Decode the signature
var sig []byte
if sig, err = DecodeSegment(signature); err != nil {
return err
}
var rsaKey *rsa.PublicKey
switch k := key.(type) {
case []byte:
if rsaKey, err = ParseRSAPublicKeyFromPEM(k); err != nil {
return err
}
case *rsa.PublicKey:
rsaKey = k
default:
return ErrInvalidKey
}
// Create hasher
if !m.Hash.Available() {
return ErrHashUnavailable
}
hasher := m.Hash.New()
hasher.Write([]byte(signingString))
// Verify the signature
return rsa.VerifyPKCS1v15(rsaKey, m.Hash, hasher.Sum(nil), sig)
}
// Implements the Sign method from SigningMethod
// For this signing method, must be either a PEM encoded PKCS1 or PKCS8 RSA private key as
// []byte, or an rsa.PrivateKey structure.
func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) {
var err error
var rsaKey *rsa.PrivateKey
switch k := key.(type) {
case []byte:
if rsaKey, err = ParseRSAPrivateKeyFromPEM(k); err != nil {
return "", err
}
case *rsa.PrivateKey:
rsaKey = k
default:
return "", ErrInvalidKey
}
// Create the hasher
if !m.Hash.Available() {
return "", ErrHashUnavailable
}
hasher := m.Hash.New()
hasher.Write([]byte(signingString))
// Sign the string and return the encoded bytes
if sigBytes, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, m.Hash, hasher.Sum(nil)); err == nil {
return EncodeSegment(sigBytes), nil
} else {
return "", err
}
}
package jwt_test
import (
"github.com/dgrijalva/jwt-go"
"io/ioutil"
"strings"
"testing"
)
var rsaTestData = []struct {
name string
tokenString string
alg string
claims map[string]interface{}
valid bool
}{
{
"Basic RS256",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
"RS256",
map[string]interface{}{"foo": "bar"},
true,
},
{
"Basic RS384",
"eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.W-jEzRfBigtCWsinvVVuldiuilzVdU5ty0MvpLaSaqK9PlAWWlDQ1VIQ_qSKzwL5IXaZkvZFJXT3yL3n7OUVu7zCNJzdwznbC8Z-b0z2lYvcklJYi2VOFRcGbJtXUqgjk2oGsiqUMUMOLP70TTefkpsgqDxbRh9CDUfpOJgW-dU7cmgaoswe3wjUAUi6B6G2YEaiuXC0XScQYSYVKIzgKXJV8Zw-7AN_DBUI4GkTpsvQ9fVVjZM9csQiEXhYekyrKu1nu_POpQonGd8yqkIyXPECNmmqH5jH4sFiF67XhD7_JpkvLziBpI-uh86evBUadmHhb9Otqw3uV3NTaXLzJw",
"RS384",
map[string]interface{}{"foo": "bar"},
true,
},
{
"Basic RS512",
"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.zBlLlmRrUxx4SJPUbV37Q1joRcI9EW13grnKduK3wtYKmDXbgDpF1cZ6B-2Jsm5RB8REmMiLpGms-EjXhgnyh2TSHE-9W2gA_jvshegLWtwRVDX40ODSkTb7OVuaWgiy9y7llvcknFBTIg-FnVPVpXMmeV_pvwQyhaz1SSwSPrDyxEmksz1hq7YONXhXPpGaNbMMeDTNP_1oj8DZaqTIL9TwV8_1wb2Odt_Fy58Ke2RVFijsOLdnyEAjt2n9Mxihu9i3PhNBkkxa2GbnXBfq3kzvZ_xxGGopLdHhJjcGWXO-NiwI9_tiu14NRv4L2xC0ItD9Yz68v2ZIZEp_DuzwRQ",
"RS512",
map[string]interface{}{"foo": "bar"},
true,
},
{
"basic invalid: foo => bar",
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.EhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg",
"RS256",
map[string]interface{}{"foo": "bar"},
false,
},
}
func TestRSAVerify(t *testing.T) {
key, _ := ioutil.ReadFile("test/sample_key.pub")
for _, data := range rsaTestData {
parts := strings.Split(data.tokenString, ".")
method := jwt.GetSigningMethod(data.alg)
err := method.Verify(strings.Join(parts[0:2], "."), parts[2], key)
if data.valid && err != nil {
t.Errorf("[%v] Error while verifying key: %v", data.name, err)
}
if !data.valid && err == nil {
t.Errorf("[%v] Invalid key passed validation", data.name)
}
}
}
func TestRSASign(t *testing.T) {
key, _ := ioutil.ReadFile("test/sample_key")
for _, data := range rsaTestData {
if data.valid {
parts := strings.Split(data.tokenString, ".")
method := jwt.GetSigningMethod(data.alg)
sig, err := method.Sign(strings.Join(parts[0:2], "."), key)
if err != nil {
t.Errorf("[%v] Error signing token: %v", data.name, err)
}
if sig != parts[2] {
t.Errorf("[%v] Incorrect signature.\nwas:\n%v\nexpecting:\n%v", data.name, sig, parts[2])
}
}
}
}
func TestRSAVerifyWithPreParsedPrivateKey(t *testing.T) {
key, _ := ioutil.ReadFile("test/sample_key.pub")
parsedKey, err := jwt.ParseRSAPublicKeyFromPEM(key)
if err != nil {
t.Fatal(err)
}
testData := rsaTestData[0]
parts := strings.Split(testData.tokenString, ".")
err = jwt.SigningMethodRS256.Verify(strings.Join(parts[0:2], "."), parts[2], parsedKey)
if err != nil {
t.Errorf("[%v] Error while verifying key: %v", testData.name, err)
}
}
func TestRSAWithPreParsedPrivateKey(t *testing.T) {
key, _ := ioutil.ReadFile("test/sample_key")
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
if err != nil {
t.Fatal(err)
}
testData := rsaTestData[0]
parts := strings.Split(testData.tokenString, ".")
sig, err := jwt.SigningMethodRS256.Sign(strings.Join(parts[0:2], "."), parsedKey)
if err != nil {
t.Errorf("[%v] Error signing token: %v", testData.name, err)
}
if sig != parts[2] {
t.Errorf("[%v] Incorrect signature.\nwas:\n%v\nexpecting:\n%v", testData.name, sig, parts[2])
}
}
func TestRSAKeyParsing(t *testing.T) {
key, _ := ioutil.ReadFile("test/sample_key")
pubKey, _ := ioutil.ReadFile("test/sample_key.pub")
badKey := []byte("All your base are belong to key")
// Test parsePrivateKey
if _, e := jwt.ParseRSAPrivateKeyFromPEM(key); e != nil {
t.Errorf("Failed to parse valid private key: %v", e)
}
if k, e := jwt.ParseRSAPrivateKeyFromPEM(pubKey); e == nil {
t.Errorf("Parsed public key as valid private key: %v", k)
}
if k, e := jwt.ParseRSAPrivateKeyFromPEM(badKey); e == nil {
t.Errorf("Parsed invalid key as valid private key: %v", k)
}
// Test parsePublicKey
if _, e := jwt.ParseRSAPublicKeyFromPEM(pubKey); e != nil {
t.Errorf("Failed to parse valid public key: %v", e)
}
if k, e := jwt.ParseRSAPublicKeyFromPEM(key); e == nil {
t.Errorf("Parsed private key as valid public key: %v", k)
}
if k, e := jwt.ParseRSAPublicKeyFromPEM(badKey); e == nil {
t.Errorf("Parsed invalid key as valid private key: %v", k)
}
}
func BenchmarkRS256Signing(b *testing.B) {
key, _ := ioutil.ReadFile("test/sample_key")
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
if err != nil {
b.Fatal(err)
}
benchmarkSigning(b, jwt.SigningMethodRS256, parsedKey)
}
func BenchmarkRS384Signing(b *testing.B) {
key, _ := ioutil.ReadFile("test/sample_key")
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
if err != nil {
b.Fatal(err)
}
benchmarkSigning(b, jwt.SigningMethodRS384, parsedKey)
}
func BenchmarkRS512Signing(b *testing.B) {
key, _ := ioutil.ReadFile("test/sample_key")
parsedKey, err := jwt.ParseRSAPrivateKeyFromPEM(key)
if err != nil {
b.Fatal(err)
}
benchmarkSigning(b, jwt.SigningMethodRS512, parsedKey)
}
package jwt
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
)
var (
ErrKeyMustBePEMEncoded = errors.New("Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key")
ErrNotRSAPrivateKey = errors.New("Key is not a valid RSA private key")
)
// Parse PEM encoded PKCS1 or PKCS8 private key
func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) {
var err error
// Parse PEM block
var block *pem.Block
if block, _ = pem.Decode(key); block == nil {
return nil, ErrKeyMustBePEMEncoded
}
var parsedKey interface{}
if parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil {
return nil, err
}
}
var pkey *rsa.PrivateKey
var ok bool
if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok {
return nil, ErrNotRSAPrivateKey
}
return pkey, nil
}
// Parse PEM encoded PKCS1 or PKCS8 public key
func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) {
var err error
// Parse PEM block
var block *pem.Block
if block, _ = pem.Decode(key); block == nil {
return nil, ErrKeyMustBePEMEncoded
}
// Parse the key
var parsedKey interface{}
if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil {
if cert, err := x509.ParseCertificate(block.Bytes); err == nil {
parsedKey = cert.PublicKey
} else {
return nil, err
}
}
var pkey *rsa.PublicKey
var ok bool
if pkey, ok = parsedKey.(*rsa.PublicKey); !ok {
return nil, ErrNotRSAPrivateKey
}
return pkey, nil
}
package jwt
var signingMethods = map[string]func() SigningMethod{}
// Signing method
type SigningMethod interface {
Verify(signingString, signature string, key interface{}) error
Sign(signingString string, key interface{}) (string, error)
Alg() string
}
// Register the "alg" name and a factory function for signing method.
// This is typically done during init() in the method's implementation
func RegisterSigningMethod(alg string, f func() SigningMethod) {
signingMethods[alg] = f
}
// Get a signing method from an "alg" string
func GetSigningMethod(alg string) (method SigningMethod) {
if methodF, ok := signingMethods[alg]; ok {
method = methodF()
}
return
}
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA4f5wg5l2hKsTeNem/V41fGnJm6gOdrj8ym3rFkEU/wT8RDtn
SgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0i
cqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20ZZuNlX2BrClciHhC
PUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR+1DcKJzQBSTAGnpYVaqpsAR
ap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKA
Rdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy7wIDAQABAoIBAQCwia1k7+2oZ2d3
n6agCAbqIE1QXfCmh41ZqJHbOY3oRQG3X1wpcGH4Gk+O+zDVTV2JszdcOt7E5dAy
MaomETAhRxB7hlIOnEN7WKm+dGNrKRvV0wDU5ReFMRHg31/Lnu8c+5BvGjZX+ky9
POIhFFYJqwCRlopGSUIxmVj5rSgtzk3iWOQXr+ah1bjEXvlxDOWkHN6YfpV5ThdE
KdBIPGEVqa63r9n2h+qazKrtiRqJqGnOrHzOECYbRFYhexsNFz7YT02xdfSHn7gM
IvabDDP/Qp0PjE1jdouiMaFHYnLBbgvlnZW9yuVf/rpXTUq/njxIXMmvmEyyvSDn
FcFikB8pAoGBAPF77hK4m3/rdGT7X8a/gwvZ2R121aBcdPwEaUhvj/36dx596zvY
mEOjrWfZhF083/nYWE2kVquj2wjs+otCLfifEEgXcVPTnEOPO9Zg3uNSL0nNQghj
FuD3iGLTUBCtM66oTe0jLSslHe8gLGEQqyMzHOzYxNqibxcOZIe8Qt0NAoGBAO+U
I5+XWjWEgDmvyC3TrOSf/KCGjtu0TSv30ipv27bDLMrpvPmD/5lpptTFwcxvVhCs
2b+chCjlghFSWFbBULBrfci2FtliClOVMYrlNBdUSJhf3aYSG2Doe6Bgt1n2CpNn
/iu37Y3NfemZBJA7hNl4dYe+f+uzM87cdQ214+jrAoGAXA0XxX8ll2+ToOLJsaNT
OvNB9h9Uc5qK5X5w+7G7O998BN2PC/MWp8H+2fVqpXgNENpNXttkRm1hk1dych86
EunfdPuqsX+as44oCyJGFHVBnWpm33eWQw9YqANRI+pCJzP08I5WK3osnPiwshd+
hR54yjgfYhBFNI7B95PmEQkCgYBzFSz7h1+s34Ycr8SvxsOBWxymG5zaCsUbPsL0
4aCgLScCHb9J+E86aVbbVFdglYa5Id7DPTL61ixhl7WZjujspeXZGSbmq0Kcnckb
mDgqkLECiOJW2NHP/j0McAkDLL4tysF8TLDO8gvuvzNC+WQ6drO2ThrypLVZQ+ry
eBIPmwKBgEZxhqa0gVvHQG/7Od69KWj4eJP28kq13RhKay8JOoN0vPmspXJo1HY3
CKuHRG+AP579dncdUnOMvfXOtkdM4vk0+hWASBQzM9xzVcztCa+koAugjVaLS9A+
9uQoqEeVNTckxx0S2bYevRy7hGQmUJTyQm3j1zEUR5jpdbL83Fbq
-----END RSA PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4f5wg5l2hKsTeNem/V41
fGnJm6gOdrj8ym3rFkEU/wT8RDtnSgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7
mCpz9Er5qLaMXJwZxzHzAahlfA0icqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBp
HssPnpYGIn20ZZuNlX2BrClciHhCPUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2
XrHhR+1DcKJzQBSTAGnpYVaqpsARap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3b
ODIRe1AuTyHceAbewn8b462yEWKARdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy
7wIDAQAB
-----END PUBLIC KEY-----
...@@ -72,7 +72,7 @@ DNS_DOMAIN="kubernetes.local" ...@@ -72,7 +72,7 @@ DNS_DOMAIN="kubernetes.local"
DNS_REPLICAS=1 DNS_REPLICAS=1
# Admission Controllers to invoke prior to persisting objects in cluster # Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
# Optional: Enable/disable public IP assignment for minions. # Optional: Enable/disable public IP assignment for minions.
# Important Note: disable only if you have setup a NAT instance for internet access and configured appropriate routes! # Important Note: disable only if you have setup a NAT instance for internet access and configured appropriate routes!
......
...@@ -49,4 +49,4 @@ ELASTICSEARCH_LOGGING_REPLICAS=1 ...@@ -49,4 +49,4 @@ ELASTICSEARCH_LOGGING_REPLICAS=1
ENABLE_CLUSTER_MONITORING="${KUBE_ENABLE_CLUSTER_MONITORING:-true}" ENABLE_CLUSTER_MONITORING="${KUBE_ENABLE_CLUSTER_MONITORING:-true}"
# Admission Controllers to invoke prior to persisting objects in cluster # Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
...@@ -76,4 +76,4 @@ DNS_DOMAIN="kubernetes.local" ...@@ -76,4 +76,4 @@ DNS_DOMAIN="kubernetes.local"
DNS_REPLICAS=1 DNS_REPLICAS=1
# Admission Controllers to invoke prior to persisting objects in cluster # Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
...@@ -74,4 +74,4 @@ DNS_SERVER_IP="10.0.0.10" ...@@ -74,4 +74,4 @@ DNS_SERVER_IP="10.0.0.10"
DNS_DOMAIN="kubernetes.local" DNS_DOMAIN="kubernetes.local"
DNS_REPLICAS=1 DNS_REPLICAS=1
ADMISSION_CONTROL=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota ADMISSION_CONTROL=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
...@@ -24,6 +24,8 @@ ...@@ -24,6 +24,8 @@
{% if grains.cloud is defined -%} {% if grains.cloud is defined -%}
{% set cloud_provider = "--cloud_provider=" + grains.cloud -%} {% set cloud_provider = "--cloud_provider=" + grains.cloud -%}
{% set service_account_key = " --service_account_private_key_file=/srv/kubernetes/server.key " -%}
{% if grains.cloud == 'gce' -%} {% if grains.cloud == 'gce' -%}
{% if grains.cloud_config is defined -%} {% if grains.cloud_config is defined -%}
{% set cloud_config = "--cloud_config=" + grains.cloud_config -%} {% set cloud_config = "--cloud_config=" + grains.cloud_config -%}
...@@ -55,7 +57,7 @@ ...@@ -55,7 +57,7 @@
{% endif -%} {% endif -%}
{% endif -%} {% endif -%}
{% set params = "--master=127.0.0.1:8080" + " " + machines + " " + cluster_name + " " + cluster_cidr + " " + allocate_node_cidrs + " " + minion_regexp + " " + cloud_provider + " " + sync_nodes + " " + cloud_config + " " + pillar['log_level'] -%} {% set params = "--master=127.0.0.1:8080" + " " + machines + " " + cluster_name + " " + cluster_cidr + " " + allocate_node_cidrs + " " + minion_regexp + " " + cloud_provider + " " + sync_nodes + " " + cloud_config + service_account_key + pillar['log_level'] -%}
{ {
"apiVersion": "v1beta3", "apiVersion": "v1beta3",
......
...@@ -50,7 +50,7 @@ MASTER_USER=vagrant ...@@ -50,7 +50,7 @@ MASTER_USER=vagrant
MASTER_PASSWD=vagrant MASTER_PASSWD=vagrant
# Admission Controllers to invoke prior to persisting objects in cluster # Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
# Optional: Install node monitoring. # Optional: Install node monitoring.
ENABLE_NODE_MONITORING=true ENABLE_NODE_MONITORING=true
......
...@@ -37,4 +37,5 @@ import ( ...@@ -37,4 +37,5 @@ import (
_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/lifecycle" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/lifecycle"
_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota"
_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny"
_ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount"
) )
...@@ -67,6 +67,8 @@ type APIServer struct { ...@@ -67,6 +67,8 @@ type APIServer struct {
BasicAuthFile string BasicAuthFile string
ClientCAFile string ClientCAFile string
TokenAuthFile string TokenAuthFile string
ServiceAccountKeyFile string
ServiceAccountLookup bool
AuthorizationMode string AuthorizationMode string
AuthorizationPolicyFile string AuthorizationPolicyFile string
AdmissionControl string AdmissionControl string
...@@ -162,6 +164,8 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) { ...@@ -162,6 +164,8 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&s.BasicAuthFile, "basic-auth-file", s.BasicAuthFile, "If set, the file that will be used to admit requests to the secure port of the API server via http basic authentication.") fs.StringVar(&s.BasicAuthFile, "basic-auth-file", s.BasicAuthFile, "If set, the file that will be used to admit requests to the secure port of the API server via http basic authentication.")
fs.StringVar(&s.ClientCAFile, "client-ca-file", s.ClientCAFile, "If set, any request presenting a client certificate signed by one of the authorities in the client-ca-file is authenticated with an identity corresponding to the CommonName of the client certificate.") fs.StringVar(&s.ClientCAFile, "client-ca-file", s.ClientCAFile, "If set, any request presenting a client certificate signed by one of the authorities in the client-ca-file is authenticated with an identity corresponding to the CommonName of the client certificate.")
fs.StringVar(&s.TokenAuthFile, "token-auth-file", s.TokenAuthFile, "If set, the file that will be used to secure the secure port of the API server via token authentication.") fs.StringVar(&s.TokenAuthFile, "token-auth-file", s.TokenAuthFile, "If set, the file that will be used to secure the secure port of the API server via token authentication.")
fs.StringVar(&s.ServiceAccountKeyFile, "service-account-key-file", s.ServiceAccountKeyFile, "File containing PEM-encoded x509 RSA private or public key, used to verify ServiceAccount tokens. If unspecified, --tls-private-key-file is used.")
fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.")
fs.StringVar(&s.AuthorizationMode, "authorization-mode", s.AuthorizationMode, "Selects how to do authorization on the secure port. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ",")) fs.StringVar(&s.AuthorizationMode, "authorization-mode", s.AuthorizationMode, "Selects how to do authorization on the secure port. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ","))
fs.StringVar(&s.AuthorizationPolicyFile, "authorization-policy-file", s.AuthorizationPolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.") fs.StringVar(&s.AuthorizationPolicyFile, "authorization-policy-file", s.AuthorizationPolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.")
fs.StringVar(&s.AdmissionControl, "admission-control", s.AdmissionControl, "Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: "+strings.Join(admission.GetPlugins(), ", ")) fs.StringVar(&s.AdmissionControl, "admission-control", s.AdmissionControl, "Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: "+strings.Join(admission.GetPlugins(), ", "))
...@@ -272,7 +276,11 @@ func (s *APIServer) Run(_ []string) error { ...@@ -272,7 +276,11 @@ func (s *APIServer) Run(_ []string) error {
n := net.IPNet(s.PortalNet) n := net.IPNet(s.PortalNet)
authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile) // Default to the private server key for service account token signing
if s.ServiceAccountKeyFile == "" && s.TLSPrivateKeyFile != "" {
s.ServiceAccountKeyFile = s.TLSPrivateKeyFile
}
authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile, s.ServiceAccountKeyFile, s.ServiceAccountLookup, client)
if err != nil { if err != nil {
glog.Fatalf("Invalid Authentication Config: %v", err) glog.Fatalf("Invalid Authentication Config: %v", err)
} }
......
...@@ -40,6 +40,7 @@ import ( ...@@ -40,6 +40,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/namespace" "github.com/GoogleCloudPlatform/kubernetes/pkg/namespace"
"github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota" "github.com/GoogleCloudPlatform/kubernetes/pkg/resourcequota"
"github.com/GoogleCloudPlatform/kubernetes/pkg/service" "github.com/GoogleCloudPlatform/kubernetes/pkg/service"
"github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder" "github.com/GoogleCloudPlatform/kubernetes/pkg/volumeclaimbinder"
...@@ -73,6 +74,7 @@ type CMServer struct { ...@@ -73,6 +74,7 @@ type CMServer struct {
PodEvictionTimeout time.Duration PodEvictionTimeout time.Duration
DeletingPodsQps float32 DeletingPodsQps float32
DeletingPodsBurst int DeletingPodsBurst int
ServiceAccountKeyFile string
// TODO: Discover these by pinging the host machines, and rip out these params. // TODO: Discover these by pinging the host machines, and rip out these params.
NodeMilliCPU int64 NodeMilliCPU int64
...@@ -141,6 +143,7 @@ func (s *CMServer) AddFlags(fs *pflag.FlagSet) { ...@@ -141,6 +143,7 @@ func (s *CMServer) AddFlags(fs *pflag.FlagSet) {
"Amount of time which we allow starting Node to be unresponsive before marking it unhealty.") "Amount of time which we allow starting Node to be unresponsive before marking it unhealty.")
fs.DurationVar(&s.NodeMonitorPeriod, "node-monitor-period", 5*time.Second, fs.DurationVar(&s.NodeMonitorPeriod, "node-monitor-period", 5*time.Second,
"The period for syncing NodeStatus in NodeController.") "The period for syncing NodeStatus in NodeController.")
fs.StringVar(&s.ServiceAccountKeyFile, "service-account-private-key-file", s.ServiceAccountKeyFile, "Filename containing a PEM-encoded private RSA key used to sign service account tokens.")
// TODO: Discover these by pinging the host machines, and rip out these flags. // TODO: Discover these by pinging the host machines, and rip out these flags.
// TODO: in the meantime, use resource.QuantityFlag() instead of these // TODO: in the meantime, use resource.QuantityFlag() instead of these
fs.Int64Var(&s.NodeMilliCPU, "node-milli-cpu", s.NodeMilliCPU, "The amount of MilliCPU provisioned on each node") fs.Int64Var(&s.NodeMilliCPU, "node-milli-cpu", s.NodeMilliCPU, "The amount of MilliCPU provisioned on each node")
...@@ -249,6 +252,25 @@ func (s *CMServer) Run(_ []string) error { ...@@ -249,6 +252,25 @@ func (s *CMServer) Run(_ []string) error {
pvclaimBinder.Run() pvclaimBinder.Run()
} }
if len(s.ServiceAccountKeyFile) > 0 {
privateKey, err := serviceaccount.ReadPrivateKey(s.ServiceAccountKeyFile)
if err != nil {
glog.Errorf("Error reading key for service account token controller: %v", err)
} else {
serviceaccount.NewTokensController(
kubeClient,
serviceaccount.DefaultTokenControllerOptions(
serviceaccount.JWTTokenGenerator(privateKey),
),
).Run()
}
}
serviceaccount.NewServiceAccountsController(
kubeClient,
serviceaccount.DefaultServiceAccountControllerOptions(),
).Run()
select {} select {}
return nil return nil
} }
...@@ -20,7 +20,7 @@ KUBE_SERVICE_ADDRESSES="--portal_net={{ kube_service_addresses }}" ...@@ -20,7 +20,7 @@ KUBE_SERVICE_ADDRESSES="--portal_net={{ kube_service_addresses }}"
KUBE_ETCD_SERVERS="--etcd_servers=http://{{ groups['etcd'][0] }}:2379" KUBE_ETCD_SERVERS="--etcd_servers=http://{{ groups['etcd'][0] }}:2379"
# default admission control policies # default admission control policies
KUBE_ADMISSION_CONTROL="--admission_control=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota" KUBE_ADMISSION_CONTROL="--admission_control=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota"
# Add your own! # Add your own!
KUBE_API_ARGS="" KUBE_API_ARGS=""
...@@ -259,6 +259,7 @@ _kubectl_get() ...@@ -259,6 +259,7 @@ _kubectl_get()
must_have_one_noun+=("resourcequota") must_have_one_noun+=("resourcequota")
must_have_one_noun+=("secret") must_have_one_noun+=("secret")
must_have_one_noun+=("service") must_have_one_noun+=("service")
must_have_one_noun+=("serviceaccount")
} }
_kubectl_describe() _kubectl_describe()
...@@ -284,7 +285,9 @@ _kubectl_describe() ...@@ -284,7 +285,9 @@ _kubectl_describe()
must_have_one_noun+=("pod") must_have_one_noun+=("pod")
must_have_one_noun+=("replicationcontroller") must_have_one_noun+=("replicationcontroller")
must_have_one_noun+=("resourcequota") must_have_one_noun+=("resourcequota")
must_have_one_noun+=("secret")
must_have_one_noun+=("service") must_have_one_noun+=("service")
must_have_one_noun+=("serviceaccount")
} }
_kubectl_create() _kubectl_create()
......
...@@ -132,12 +132,21 @@ trap cleanup EXIT ...@@ -132,12 +132,21 @@ trap cleanup EXIT
echo "Starting etcd" echo "Starting etcd"
kube::etcd::start kube::etcd::start
SERVICE_ACCOUNT_LOOKUP=${SERVICE_ACCOUNT_LOOKUP:-false}
SERVICE_ACCOUNT_KEY=${SERVICE_ACCOUNT_KEY:-"/var/run/kubernetes/serviceaccount.key"}
# Generate ServiceAccount key if needed
if [[ ! -f "${SERVICE_ACCOUNT_KEY}" ]]; then
openssl genrsa -out "${SERVICE_ACCOUNT_KEY}" 2048 2>/dev/null
fi
# Admission Controllers to invoke prior to persisting objects in cluster # Admission Controllers to invoke prior to persisting objects in cluster
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota
APISERVER_LOG=/tmp/kube-apiserver.log APISERVER_LOG=/tmp/kube-apiserver.log
sudo -E "${GO_OUT}/kube-apiserver" \ sudo -E "${GO_OUT}/kube-apiserver" \
--v=${LOG_LEVEL} \ --v=${LOG_LEVEL} \
--service_account_key_file="${SERVICE_ACCOUNT_KEY}" \
--service_account_lookup="${SERVICE_ACCOUNT_LOOKUP}" \
--admission_control="${ADMISSION_CONTROL}" \ --admission_control="${ADMISSION_CONTROL}" \
--address="${API_HOST}" \ --address="${API_HOST}" \
--port="${API_PORT}" \ --port="${API_PORT}" \
...@@ -155,6 +164,7 @@ CTLRMGR_LOG=/tmp/kube-controller-manager.log ...@@ -155,6 +164,7 @@ CTLRMGR_LOG=/tmp/kube-controller-manager.log
sudo -E "${GO_OUT}/kube-controller-manager" \ sudo -E "${GO_OUT}/kube-controller-manager" \
--v=${LOG_LEVEL} \ --v=${LOG_LEVEL} \
--machines="127.0.0.1" \ --machines="127.0.0.1" \
--service_account_private_key_file="${SERVICE_ACCOUNT_KEY}" \
--master="${API_HOST}:${API_PORT}" >"${CTLRMGR_LOG}" 2>&1 & --master="${API_HOST}:${API_PORT}" >"${CTLRMGR_LOG}" 2>&1 &
CTLRMGR_PID=$! CTLRMGR_PID=$!
......
...@@ -51,6 +51,8 @@ func init() { ...@@ -51,6 +51,8 @@ func init() {
&ResourceQuotaList{}, &ResourceQuotaList{},
&Namespace{}, &Namespace{},
&NamespaceList{}, &NamespaceList{},
&ServiceAccount{},
&ServiceAccountList{},
&Secret{}, &Secret{},
&SecretList{}, &SecretList{},
&PersistentVolume{}, &PersistentVolume{},
...@@ -98,6 +100,8 @@ func (*ResourceQuota) IsAnAPIObject() {} ...@@ -98,6 +100,8 @@ func (*ResourceQuota) IsAnAPIObject() {}
func (*ResourceQuotaList) IsAnAPIObject() {} func (*ResourceQuotaList) IsAnAPIObject() {}
func (*Namespace) IsAnAPIObject() {} func (*Namespace) IsAnAPIObject() {}
func (*NamespaceList) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {}
func (*ServiceAccount) IsAnAPIObject() {}
func (*ServiceAccountList) IsAnAPIObject() {}
func (*Secret) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {}
func (*SecretList) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {}
func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {}
......
...@@ -814,6 +814,10 @@ type PodSpec struct { ...@@ -814,6 +814,10 @@ type PodSpec struct {
// NodeSelector is a selector which must be true for the pod to fit on a node // NodeSelector is a selector which must be true for the pod to fit on a node
NodeSelector map[string]string `json:"nodeSelector,omitempty"` NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// ServiceAccount is the name of the ServiceAccount to use to run this pod
// The pod will be allowed to use secrets referenced by the ServiceAccount
ServiceAccount string `json:"serviceAccount"`
// Host is a request to schedule this pod onto a specific host. If it is non-empty, // Host is a request to schedule this pod onto a specific host. If it is non-empty,
// the the scheduler simply schedules this pod onto that host, assuming that it fits // the the scheduler simply schedules this pod onto that host, assuming that it fits
// resource requirements. // resource requirements.
...@@ -1035,6 +1039,26 @@ type Service struct { ...@@ -1035,6 +1039,26 @@ type Service struct {
Status ServiceStatus `json:"status,omitempty"` Status ServiceStatus `json:"status,omitempty"`
} }
// ServiceAccount binds together:
// * a name, understood by users, and perhaps by peripheral systems, for an identity
// * a principal that can be authenticated and authorized
// * a set of secrets
type ServiceAccount struct {
TypeMeta `json:",inline"`
ObjectMeta `json:"metadata,omitempty"`
// Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount
Secrets []ObjectReference `json:"secrets"`
}
// ServiceAccountList is a list of ServiceAccount objects
type ServiceAccountList struct {
TypeMeta `json:",inline"`
ListMeta `json:"metadata,omitempty"`
Items []ServiceAccount `json:"items"`
}
// Endpoints is a collection of endpoints that implement the actual service. Example: // Endpoints is a collection of endpoints that implement the actual service. Example:
// Name: "mysvc", // Name: "mysvc",
// Subsets: [ // Subsets: [
...@@ -1805,7 +1829,25 @@ const MaxSecretSize = 1 * 1024 * 1024 ...@@ -1805,7 +1829,25 @@ const MaxSecretSize = 1 * 1024 * 1024
type SecretType string type SecretType string
const ( const (
SecretTypeOpaque SecretType = "Opaque" // Default; arbitrary user-defined data // SecretTypeOpaque is the default; arbitrary user-defined data
SecretTypeOpaque SecretType = "Opaque"
// SecretTypeServiceAccountToken contains a token that identifies a service account to the API
//
// Required fields:
// - Secret.Annotations["kubernetes.io/service-account.name"] - the name of the ServiceAccount the token identifies
// - Secret.Annotations["kubernetes.io/service-account.uid"] - the UID of the ServiceAccount the token identifies
// - Secret.Data["token"] - a token that identifies the service account to the API
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
ServiceAccountNameKey = "kubernetes.io/service-account.name"
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
ServiceAccountUIDKey = "kubernetes.io/service-account.uid"
// ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets
ServiceAccountTokenKey = "token"
// ServiceAccountKubeconfigKey is the key of the optional kubeconfig data for SecretTypeServiceAccountToken secrets
ServiceAccountKubeconfigKey = "kubernetes.kubeconfig"
) )
type SecretList struct { type SecretList struct {
......
...@@ -1880,6 +1880,7 @@ func init() { ...@@ -1880,6 +1880,7 @@ func init() {
out.NodeSelector[key] = val out.NodeSelector[key] = val
} }
} }
out.ServiceAccount = in.ServiceAccount
out.Host = in.Host out.Host = in.Host
out.HostNetwork = in.HostNetwork out.HostNetwork = in.HostNetwork
return nil return nil
...@@ -1913,6 +1914,7 @@ func init() { ...@@ -1913,6 +1914,7 @@ func init() {
out.NodeSelector[key] = val out.NodeSelector[key] = val
} }
} }
out.ServiceAccount = in.ServiceAccount
out.Host = in.Host out.Host = in.Host
out.HostNetwork = in.HostNetwork out.HostNetwork = in.HostNetwork
return nil return nil
...@@ -2477,6 +2479,74 @@ func init() { ...@@ -2477,6 +2479,74 @@ func init() {
} }
return nil return nil
}, },
func(in *ServiceAccount, out *newer.ServiceAccount, s conversion.Scope) error {
if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil {
return err
}
if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil {
return err
}
if in.Secrets != nil {
out.Secrets = make([]newer.ObjectReference, len(in.Secrets))
for i := range in.Secrets {
if err := s.Convert(&in.Secrets[i], &out.Secrets[i], 0); err != nil {
return err
}
}
}
return nil
},
func(in *newer.ServiceAccount, out *ServiceAccount, s conversion.Scope) error {
if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil {
return err
}
if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil {
return err
}
if in.Secrets != nil {
out.Secrets = make([]ObjectReference, len(in.Secrets))
for i := range in.Secrets {
if err := s.Convert(&in.Secrets[i], &out.Secrets[i], 0); err != nil {
return err
}
}
}
return nil
},
func(in *ServiceAccountList, out *newer.ServiceAccountList, s conversion.Scope) error {
if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil {
return err
}
if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil {
return err
}
if in.Items != nil {
out.Items = make([]newer.ServiceAccount, len(in.Items))
for i := range in.Items {
if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil {
return err
}
}
}
return nil
},
func(in *newer.ServiceAccountList, out *ServiceAccountList, s conversion.Scope) error {
if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil {
return err
}
if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil {
return err
}
if in.Items != nil {
out.Items = make([]ServiceAccount, len(in.Items))
for i := range in.Items {
if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil {
return err
}
}
}
return nil
},
func(in *ServiceList, out *newer.ServiceList, s conversion.Scope) error { func(in *ServiceList, out *newer.ServiceList, s conversion.Scope) error {
if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil {
return err return err
......
...@@ -52,6 +52,8 @@ func init() { ...@@ -52,6 +52,8 @@ func init() {
&NamespaceList{}, &NamespaceList{},
&Secret{}, &Secret{},
&SecretList{}, &SecretList{},
&ServiceAccount{},
&ServiceAccountList{},
&PersistentVolume{}, &PersistentVolume{},
&PersistentVolumeList{}, &PersistentVolumeList{},
&PersistentVolumeClaim{}, &PersistentVolumeClaim{},
...@@ -97,6 +99,8 @@ func (*Namespace) IsAnAPIObject() {} ...@@ -97,6 +99,8 @@ func (*Namespace) IsAnAPIObject() {}
func (*NamespaceList) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {}
func (*Secret) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {}
func (*SecretList) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {}
func (*ServiceAccount) IsAnAPIObject() {}
func (*ServiceAccountList) IsAnAPIObject() {}
func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {}
func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeList) IsAnAPIObject() {}
func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {}
......
...@@ -816,6 +816,9 @@ type PodSpec struct { ...@@ -816,6 +816,9 @@ type PodSpec struct {
// NodeSelector is a selector which must be true for the pod to fit on a node // NodeSelector is a selector which must be true for the pod to fit on a node
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"` NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"`
// ServiceAccount is the name of the ServiceAccount to use to run this pod
ServiceAccount string `json:"serviceAccount" description:"name of the ServiceAccount to use to run this pod"`
// Host is a request to schedule this pod onto a specific host. If it is non-empty, // Host is a request to schedule this pod onto a specific host. If it is non-empty,
// the the scheduler simply schedules this pod onto that host, assuming that it fits // the the scheduler simply schedules this pod onto that host, assuming that it fits
// resource requirements. // resource requirements.
...@@ -1036,6 +1039,26 @@ type ServiceList struct { ...@@ -1036,6 +1039,26 @@ type ServiceList struct {
Items []Service `json:"items" description:"list of services"` Items []Service `json:"items" description:"list of services"`
} }
// ServiceAccount binds together:
// * a name, understood by users, and perhaps by peripheral systems, for an identity
// * a principal that can be authenticated and authorized
// * a set of secrets
type ServiceAccount struct {
TypeMeta `json:",inline"`
ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see http://docs.k8s.io/api-conventions.md#metadata"`
// Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount
Secrets []ObjectReference `json:"secrets" description:"list of secrets that can be used by pods running as this service account" patchStrategy:"merge" patchMergeKey:"name"`
}
// ServiceAccountList is a list of ServiceAccount objects
type ServiceAccountList struct {
TypeMeta `json:",inline"`
ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://docs.k8s.io/api-conventions.md#metadata"`
Items []ServiceAccount `json:"items" description:"list of ServiceAccounts"`
}
// Endpoints is a collection of endpoints that implement the actual service. Example: // Endpoints is a collection of endpoints that implement the actual service. Example:
// Name: "mysvc", // Name: "mysvc",
// Subsets: [ // Subsets: [
...@@ -1708,7 +1731,23 @@ const MaxSecretSize = 1 * 1024 * 1024 ...@@ -1708,7 +1731,23 @@ const MaxSecretSize = 1 * 1024 * 1024
type SecretType string type SecretType string
const ( const (
SecretTypeOpaque SecretType = "Opaque" // Default; arbitrary user-defined data // SecretTypeOpaque is the default; arbitrary user-defined data
SecretTypeOpaque SecretType = "Opaque"
// SecretTypeServiceAccountToken contains a token that identifies a service account to the API
//
// Required fields:
// - Secret.Annotations["kubernetes.io/service-account.name"] - the name of the ServiceAccount the token identifies
// - Secret.Annotations["kubernetes.io/service-account.uid"] - the UID of the ServiceAccount the token identifies
// - Secret.Data["token"] - a token that identifies the service account to the API
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
ServiceAccountNameKey = "kubernetes.io/service-account.name"
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
ServiceAccountUIDKey = "kubernetes.io/service-account.uid"
// ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets
ServiceAccountTokenKey = "token"
) )
type SecretList struct { type SecretList struct {
......
...@@ -377,6 +377,7 @@ func init() { ...@@ -377,6 +377,7 @@ func init() {
} }
out.DesiredState.Host = in.Spec.Host out.DesiredState.Host = in.Spec.Host
out.CurrentState.Host = in.Spec.Host out.CurrentState.Host = in.Spec.Host
out.ServiceAccount = in.Spec.ServiceAccount
if err := s.Convert(&in.Status, &out.CurrentState, 0); err != nil { if err := s.Convert(&in.Status, &out.CurrentState, 0); err != nil {
return err return err
} }
...@@ -399,6 +400,7 @@ func init() { ...@@ -399,6 +400,7 @@ func init() {
return err return err
} }
out.Spec.Host = in.DesiredState.Host out.Spec.Host = in.DesiredState.Host
out.Spec.ServiceAccount = in.ServiceAccount
if err := s.Convert(&in.CurrentState, &out.Status, 0); err != nil { if err := s.Convert(&in.CurrentState, &out.Status, 0); err != nil {
return err return err
} }
...@@ -503,6 +505,7 @@ func init() { ...@@ -503,6 +505,7 @@ func init() {
return err return err
} }
out.DesiredState.Host = in.Spec.Host out.DesiredState.Host = in.Spec.Host
out.ServiceAccount = in.Spec.ServiceAccount
if err := s.Convert(&in.Spec.NodeSelector, &out.NodeSelector, 0); err != nil { if err := s.Convert(&in.Spec.NodeSelector, &out.NodeSelector, 0); err != nil {
return err return err
} }
...@@ -519,6 +522,7 @@ func init() { ...@@ -519,6 +522,7 @@ func init() {
return err return err
} }
out.Spec.Host = in.DesiredState.Host out.Spec.Host = in.DesiredState.Host
out.Spec.ServiceAccount = in.ServiceAccount
if err := s.Convert(&in.NodeSelector, &out.Spec.NodeSelector, 0); err != nil { if err := s.Convert(&in.NodeSelector, &out.Spec.NodeSelector, 0); err != nil {
return err return err
} }
...@@ -1685,4 +1689,17 @@ func init() { ...@@ -1685,4 +1689,17 @@ func init() {
// If one of the conversion functions is malformed, detect it immediately. // If one of the conversion functions is malformed, detect it immediately.
panic(err) panic(err)
} }
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta1", "ServiceAccount",
func(label, value string) (string, string, error) {
switch label {
case "name":
return "metadata.name", value, nil
default:
return "", "", fmt.Errorf("field label not supported: %s", label)
}
})
if err != nil {
// If one of the conversion functions is malformed, detect it immediately.
panic(err)
}
} }
...@@ -59,6 +59,8 @@ func init() { ...@@ -59,6 +59,8 @@ func init() {
&NamespaceList{}, &NamespaceList{},
&Secret{}, &Secret{},
&SecretList{}, &SecretList{},
&ServiceAccount{},
&ServiceAccountList{},
&PersistentVolume{}, &PersistentVolume{},
&PersistentVolumeList{}, &PersistentVolumeList{},
&PersistentVolumeClaim{}, &PersistentVolumeClaim{},
...@@ -105,6 +107,8 @@ func (*Namespace) IsAnAPIObject() {} ...@@ -105,6 +107,8 @@ func (*Namespace) IsAnAPIObject() {}
func (*NamespaceList) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {}
func (*Secret) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {}
func (*SecretList) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {}
func (*ServiceAccount) IsAnAPIObject() {}
func (*ServiceAccountList) IsAnAPIObject() {}
func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {}
func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeList) IsAnAPIObject() {}
func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {}
......
...@@ -755,6 +755,8 @@ type Pod struct { ...@@ -755,6 +755,8 @@ type Pod struct {
Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize pods; may match selectors of replication controllers and services"` Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize pods; may match selectors of replication controllers and services"`
DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of the pod"` DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of the pod"`
CurrentState PodState `json:"currentState,omitempty" description:"current state of the pod; populated by the system, read-only"` CurrentState PodState `json:"currentState,omitempty" description:"current state of the pod; populated by the system, read-only"`
// ServiceAccount is the name of the ServiceAccount to use to run this pod
ServiceAccount string `json:"serviceAccount,omitempty" description:"the name of the ServiceAccount to use to run this pod"`
// NodeSelector is a selector which must be true for the pod to fit on a node // NodeSelector is a selector which must be true for the pod to fit on a node
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"` NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"`
} }
...@@ -782,10 +784,11 @@ type ReplicationController struct { ...@@ -782,10 +784,11 @@ type ReplicationController struct {
// PodTemplate holds the information used for creating pods. // PodTemplate holds the information used for creating pods.
type PodTemplate struct { type PodTemplate struct {
DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"` DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"`
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"` ServiceAccount string `json:"serviceAccount,omitempty" description:"the name of the ServiceAccount to use to run this pod"`
Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize the pods created from the template; must match the selector of the replication controller to which the template belongs; may match selectors of services"` NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"`
Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about pods created from the template"` Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize the pods created from the template; must match the selector of the replication controller to which the template belongs; may match selectors of services"`
Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about pods created from the template"`
} }
// Session Affinity Type string // Session Affinity Type string
...@@ -884,6 +887,24 @@ type ServicePort struct { ...@@ -884,6 +887,24 @@ type ServicePort struct {
ContainerPort util.IntOrString `json:"containerPort" description:"the port to access on the containers belonging to pods targeted by the service; defaults to the service port"` ContainerPort util.IntOrString `json:"containerPort" description:"the port to access on the containers belonging to pods targeted by the service; defaults to the service port"`
} }
// ServiceAccount binds together:
// * a name, understood by users, and perhaps by peripheral systems, for an identity
// * a principal that can be authenticated and authorized
// * a set of secrets
type ServiceAccount struct {
TypeMeta `json:",inline"`
// Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount
Secrets []ObjectReference `json:"secrets" description:"list of secrets that can be used by pods running as this service account" patchStrategy:"merge" patchMergeKey:"name"`
}
// ServiceAccountList is a list of ServiceAccount objects
type ServiceAccountList struct {
TypeMeta `json:",inline"`
Items []ServiceAccount `json:"items" description:"list of ServiceAccounts"`
}
// EndpointObjectReference is a reference to an object exposing the endpoint // EndpointObjectReference is a reference to an object exposing the endpoint
type EndpointObjectReference struct { type EndpointObjectReference struct {
Endpoint string `json:"endpoint" description:"endpoint exposed by the referenced object"` Endpoint string `json:"endpoint" description:"endpoint exposed by the referenced object"`
...@@ -1617,7 +1638,23 @@ const MaxSecretSize = 1 * 1024 * 1024 ...@@ -1617,7 +1638,23 @@ const MaxSecretSize = 1 * 1024 * 1024
type SecretType string type SecretType string
const ( const (
SecretTypeOpaque SecretType = "Opaque" // Default; arbitrary user-defined data // SecretTypeOpaque is the default; arbitrary user-defined data
SecretTypeOpaque SecretType = "Opaque"
// SecretTypeServiceAccountToken contains a token that identifies a service account to the API
//
// Required fields:
// - Secret.Annotations["kubernetes.io/service-account.name"] - the name of the ServiceAccount the token identifies
// - Secret.Annotations["kubernetes.io/service-account.uid"] - the UID of the ServiceAccount the token identifies
// - Secret.Data["token"] - a token that identifies the service account to the API
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
ServiceAccountNameKey = "kubernetes.io/service-account.name"
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
ServiceAccountUIDKey = "kubernetes.io/service-account.uid"
// ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets
ServiceAccountTokenKey = "token"
) )
type SecretList struct { type SecretList struct {
......
...@@ -180,6 +180,7 @@ func init() { ...@@ -180,6 +180,7 @@ func init() {
} }
out.DesiredState.Host = in.Spec.Host out.DesiredState.Host = in.Spec.Host
out.CurrentState.Host = in.Spec.Host out.CurrentState.Host = in.Spec.Host
out.ServiceAccount = in.Spec.ServiceAccount
if err := s.Convert(&in.Status, &out.CurrentState, 0); err != nil { if err := s.Convert(&in.Status, &out.CurrentState, 0); err != nil {
return err return err
} }
...@@ -201,6 +202,7 @@ func init() { ...@@ -201,6 +202,7 @@ func init() {
if err := s.Convert(&in.DesiredState.Manifest, &out.Spec, 0); err != nil { if err := s.Convert(&in.DesiredState.Manifest, &out.Spec, 0); err != nil {
return err return err
} }
out.Spec.ServiceAccount = in.ServiceAccount
out.Spec.Host = in.DesiredState.Host out.Spec.Host = in.DesiredState.Host
if err := s.Convert(&in.CurrentState, &out.Status, 0); err != nil { if err := s.Convert(&in.CurrentState, &out.Status, 0); err != nil {
return err return err
...@@ -282,6 +284,7 @@ func init() { ...@@ -282,6 +284,7 @@ func init() {
return err return err
} }
out.DesiredState.Host = in.Spec.Host out.DesiredState.Host = in.Spec.Host
out.ServiceAccount = in.Spec.ServiceAccount
if err := s.Convert(&in.Spec.NodeSelector, &out.NodeSelector, 0); err != nil { if err := s.Convert(&in.Spec.NodeSelector, &out.NodeSelector, 0); err != nil {
return err return err
} }
...@@ -298,6 +301,7 @@ func init() { ...@@ -298,6 +301,7 @@ func init() {
return err return err
} }
out.Spec.Host = in.DesiredState.Host out.Spec.Host = in.DesiredState.Host
out.Spec.ServiceAccount = in.ServiceAccount
if err := s.Convert(&in.NodeSelector, &out.Spec.NodeSelector, 0); err != nil { if err := s.Convert(&in.NodeSelector, &out.Spec.NodeSelector, 0); err != nil {
return err return err
} }
...@@ -1601,4 +1605,17 @@ func init() { ...@@ -1601,4 +1605,17 @@ func init() {
// If one of the conversion functions is malformed, detect it immediately. // If one of the conversion functions is malformed, detect it immediately.
panic(err) panic(err)
} }
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta2", "ServiceAccount",
func(label, value string) (string, string, error) {
switch label {
case "name":
return "metadata.name", value, nil
default:
return "", "", fmt.Errorf("field label not supported: %s", label)
}
})
if err != nil {
// If one of the conversion functions is malformed, detect it immediately.
panic(err)
}
} }
...@@ -59,6 +59,8 @@ func init() { ...@@ -59,6 +59,8 @@ func init() {
&NamespaceList{}, &NamespaceList{},
&Secret{}, &Secret{},
&SecretList{}, &SecretList{},
&ServiceAccount{},
&ServiceAccountList{},
&PersistentVolume{}, &PersistentVolume{},
&PersistentVolumeList{}, &PersistentVolumeList{},
&PersistentVolumeClaim{}, &PersistentVolumeClaim{},
...@@ -105,6 +107,8 @@ func (*Namespace) IsAnAPIObject() {} ...@@ -105,6 +107,8 @@ func (*Namespace) IsAnAPIObject() {}
func (*NamespaceList) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {}
func (*Secret) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {}
func (*SecretList) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {}
func (*ServiceAccount) IsAnAPIObject() {}
func (*ServiceAccountList) IsAnAPIObject() {}
func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {}
func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeList) IsAnAPIObject() {}
func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {}
......
...@@ -756,6 +756,8 @@ type Pod struct { ...@@ -756,6 +756,8 @@ type Pod struct {
Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize pods; may match selectors of replication controllers and services"` Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize pods; may match selectors of replication controllers and services"`
DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of the pod"` DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of the pod"`
CurrentState PodState `json:"currentState,omitempty" description:"current state of the pod; populated by the system, read-only"` CurrentState PodState `json:"currentState,omitempty" description:"current state of the pod; populated by the system, read-only"`
// ServiceAccount is the name of the ServiceAccount to use to run this pod
ServiceAccount string `json:"serviceAccount,omitempty" description:"the name of the ServiceAccount to use to run this pod"`
// NodeSelector is a selector which must be true for the pod to fit on a node // NodeSelector is a selector which must be true for the pod to fit on a node
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"` NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"`
} }
...@@ -787,10 +789,11 @@ type ReplicationController struct { ...@@ -787,10 +789,11 @@ type ReplicationController struct {
// //
// http://docs.k8s.io/replication-controller.md#pod-template // http://docs.k8s.io/replication-controller.md#pod-template
type PodTemplate struct { type PodTemplate struct {
DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"` DesiredState PodState `json:"desiredState,omitempty" description:"specification of the desired state of pods created from this template"`
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"` ServiceAccount string `json:"serviceAccount,omitempty" description:"the name of the ServiceAccount to use to run this pod"`
Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize the pods created from the template; must match the selector of the replication controller to which the template belongs; may match selectors of services"` NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"a selector which must be true for the pod to fit on a node"`
Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about pods created from the template"` Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize the pods created from the template; must match the selector of the replication controller to which the template belongs; may match selectors of services"`
Annotations map[string]string `json:"annotations,omitempty" description:"map of string keys and values that can be used by external tooling to store and retrieve arbitrary metadata about pods created from the template"`
} }
// Session Affinity Type string // Session Affinity Type string
...@@ -891,6 +894,24 @@ type ServicePort struct { ...@@ -891,6 +894,24 @@ type ServicePort struct {
ContainerPort util.IntOrString `json:"containerPort" description:"the port to access on the containers belonging to pods targeted by the service; defaults to the service port"` ContainerPort util.IntOrString `json:"containerPort" description:"the port to access on the containers belonging to pods targeted by the service; defaults to the service port"`
} }
// ServiceAccount binds together:
// * a name, understood by users, and perhaps by peripheral systems, for an identity
// * a principal that can be authenticated and authorized
// * a set of secrets
type ServiceAccount struct {
TypeMeta `json:",inline"`
// Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount
Secrets []ObjectReference `json:"secrets" description:"list of secrets that can be used by pods running as this service account" patchStrategy:"merge" patchMergeKey:"name"`
}
// ServiceAccountList is a list of ServiceAccount objects
type ServiceAccountList struct {
TypeMeta `json:",inline"`
Items []ServiceAccount `json:"items" description:"list of ServiceAccounts"`
}
// EndpointObjectReference is a reference to an object exposing the endpoint // EndpointObjectReference is a reference to an object exposing the endpoint
type EndpointObjectReference struct { type EndpointObjectReference struct {
Endpoint string `json:"endpoint" description:"endpoint exposed by the referenced object"` Endpoint string `json:"endpoint" description:"endpoint exposed by the referenced object"`
...@@ -1692,7 +1713,23 @@ const MaxSecretSize = 1 * 1024 * 1024 ...@@ -1692,7 +1713,23 @@ const MaxSecretSize = 1 * 1024 * 1024
type SecretType string type SecretType string
const ( const (
SecretTypeOpaque SecretType = "Opaque" // Default; arbitrary user-defined data // SecretTypeOpaque is the default; arbitrary user-defined data
SecretTypeOpaque SecretType = "Opaque"
// SecretTypeServiceAccountToken contains a token that identifies a service account to the API
//
// Required fields:
// - Secret.Annotations["kubernetes.io/service-account.name"] - the name of the ServiceAccount the token identifies
// - Secret.Annotations["kubernetes.io/service-account.uid"] - the UID of the ServiceAccount the token identifies
// - Secret.Data["token"] - a token that identifies the service account to the API
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
ServiceAccountNameKey = "kubernetes.io/service-account.name"
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
ServiceAccountUIDKey = "kubernetes.io/service-account.uid"
// ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets
ServiceAccountTokenKey = "token"
) )
type SecretList struct { type SecretList struct {
......
...@@ -128,6 +128,19 @@ func init() { ...@@ -128,6 +128,19 @@ func init() {
// If one of the conversion functions is malformed, detect it immediately. // If one of the conversion functions is malformed, detect it immediately.
panic(err) panic(err)
} }
err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "ServiceAccount",
func(label, value string) (string, string, error) {
switch label {
case "metadata.name":
return label, value, nil
default:
return "", "", fmt.Errorf("field label not supported: %s", label)
}
})
if err != nil {
// If one of the conversion functions is malformed, detect it immediately.
panic(err)
}
} }
func convert_v1beta3_Container_To_api_Container(in *Container, out *newer.Container, s conversion.Scope) error { func convert_v1beta3_Container_To_api_Container(in *Container, out *newer.Container, s conversion.Scope) error {
......
...@@ -2639,6 +2639,7 @@ func convert_v1beta3_PodSpec_To_api_PodSpec(in *PodSpec, out *newer.PodSpec, s c ...@@ -2639,6 +2639,7 @@ func convert_v1beta3_PodSpec_To_api_PodSpec(in *PodSpec, out *newer.PodSpec, s c
} else { } else {
out.NodeSelector = nil out.NodeSelector = nil
} }
out.ServiceAccount = in.ServiceAccount
out.Host = in.Host out.Host = in.Host
out.HostNetwork = in.HostNetwork out.HostNetwork = in.HostNetwork
return nil return nil
...@@ -2684,6 +2685,7 @@ func convert_api_PodSpec_To_v1beta3_PodSpec(in *newer.PodSpec, out *PodSpec, s c ...@@ -2684,6 +2685,7 @@ func convert_api_PodSpec_To_v1beta3_PodSpec(in *newer.PodSpec, out *PodSpec, s c
} else { } else {
out.NodeSelector = nil out.NodeSelector = nil
} }
out.ServiceAccount = in.ServiceAccount
out.Host = in.Host out.Host = in.Host
out.HostNetwork = in.HostNetwork out.HostNetwork = in.HostNetwork
return nil return nil
...@@ -3625,6 +3627,98 @@ func convert_api_Service_To_v1beta3_Service(in *newer.Service, out *Service, s c ...@@ -3625,6 +3627,98 @@ func convert_api_Service_To_v1beta3_Service(in *newer.Service, out *Service, s c
return nil return nil
} }
func convert_v1beta3_ServiceAccount_To_api_ServiceAccount(in *ServiceAccount, out *newer.ServiceAccount, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*ServiceAccount))(in)
}
if err := convert_v1beta3_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil {
return err
}
if err := convert_v1beta3_ObjectMeta_To_api_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil {
return err
}
if in.Secrets != nil {
out.Secrets = make([]newer.ObjectReference, len(in.Secrets))
for i := range in.Secrets {
if err := convert_v1beta3_ObjectReference_To_api_ObjectReference(&in.Secrets[i], &out.Secrets[i], s); err != nil {
return err
}
}
} else {
out.Secrets = nil
}
return nil
}
func convert_api_ServiceAccount_To_v1beta3_ServiceAccount(in *newer.ServiceAccount, out *ServiceAccount, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*newer.ServiceAccount))(in)
}
if err := convert_api_TypeMeta_To_v1beta3_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil {
return err
}
if err := convert_api_ObjectMeta_To_v1beta3_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil {
return err
}
if in.Secrets != nil {
out.Secrets = make([]ObjectReference, len(in.Secrets))
for i := range in.Secrets {
if err := convert_api_ObjectReference_To_v1beta3_ObjectReference(&in.Secrets[i], &out.Secrets[i], s); err != nil {
return err
}
}
} else {
out.Secrets = nil
}
return nil
}
func convert_v1beta3_ServiceAccountList_To_api_ServiceAccountList(in *ServiceAccountList, out *newer.ServiceAccountList, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*ServiceAccountList))(in)
}
if err := convert_v1beta3_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil {
return err
}
if err := convert_v1beta3_ListMeta_To_api_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil {
return err
}
if in.Items != nil {
out.Items = make([]newer.ServiceAccount, len(in.Items))
for i := range in.Items {
if err := convert_v1beta3_ServiceAccount_To_api_ServiceAccount(&in.Items[i], &out.Items[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
return nil
}
func convert_api_ServiceAccountList_To_v1beta3_ServiceAccountList(in *newer.ServiceAccountList, out *ServiceAccountList, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*newer.ServiceAccountList))(in)
}
if err := convert_api_TypeMeta_To_v1beta3_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil {
return err
}
if err := convert_api_ListMeta_To_v1beta3_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil {
return err
}
if in.Items != nil {
out.Items = make([]ServiceAccount, len(in.Items))
for i := range in.Items {
if err := convert_api_ServiceAccount_To_v1beta3_ServiceAccount(&in.Items[i], &out.Items[i], s); err != nil {
return err
}
}
} else {
out.Items = nil
}
return nil
}
func convert_v1beta3_ServiceList_To_api_ServiceList(in *ServiceList, out *newer.ServiceList, s conversion.Scope) error { func convert_v1beta3_ServiceList_To_api_ServiceList(in *ServiceList, out *newer.ServiceList, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*ServiceList))(in) defaulting.(func(*ServiceList))(in)
...@@ -4244,6 +4338,8 @@ func init() { ...@@ -4244,6 +4338,8 @@ func init() {
convert_api_Secret_To_v1beta3_Secret, convert_api_Secret_To_v1beta3_Secret,
convert_api_SecurityContext_To_v1beta3_SecurityContext, convert_api_SecurityContext_To_v1beta3_SecurityContext,
convert_api_SerializedReference_To_v1beta3_SerializedReference, convert_api_SerializedReference_To_v1beta3_SerializedReference,
convert_api_ServiceAccountList_To_v1beta3_ServiceAccountList,
convert_api_ServiceAccount_To_v1beta3_ServiceAccount,
convert_api_ServiceList_To_v1beta3_ServiceList, convert_api_ServiceList_To_v1beta3_ServiceList,
convert_api_ServicePort_To_v1beta3_ServicePort, convert_api_ServicePort_To_v1beta3_ServicePort,
convert_api_ServiceSpec_To_v1beta3_ServiceSpec, convert_api_ServiceSpec_To_v1beta3_ServiceSpec,
...@@ -4350,6 +4446,8 @@ func init() { ...@@ -4350,6 +4446,8 @@ func init() {
convert_v1beta3_Secret_To_api_Secret, convert_v1beta3_Secret_To_api_Secret,
convert_v1beta3_SecurityContext_To_api_SecurityContext, convert_v1beta3_SecurityContext_To_api_SecurityContext,
convert_v1beta3_SerializedReference_To_api_SerializedReference, convert_v1beta3_SerializedReference_To_api_SerializedReference,
convert_v1beta3_ServiceAccountList_To_api_ServiceAccountList,
convert_v1beta3_ServiceAccount_To_api_ServiceAccount,
convert_v1beta3_ServiceList_To_api_ServiceList, convert_v1beta3_ServiceList_To_api_ServiceList,
convert_v1beta3_ServicePort_To_api_ServicePort, convert_v1beta3_ServicePort_To_api_ServicePort,
convert_v1beta3_ServiceSpec_To_api_ServiceSpec, convert_v1beta3_ServiceSpec_To_api_ServiceSpec,
......
...@@ -52,6 +52,8 @@ func init() { ...@@ -52,6 +52,8 @@ func init() {
&NamespaceList{}, &NamespaceList{},
&Secret{}, &Secret{},
&SecretList{}, &SecretList{},
&ServiceAccount{},
&ServiceAccountList{},
&PersistentVolume{}, &PersistentVolume{},
&PersistentVolumeList{}, &PersistentVolumeList{},
&PersistentVolumeClaim{}, &PersistentVolumeClaim{},
...@@ -97,6 +99,8 @@ func (*Namespace) IsAnAPIObject() {} ...@@ -97,6 +99,8 @@ func (*Namespace) IsAnAPIObject() {}
func (*NamespaceList) IsAnAPIObject() {} func (*NamespaceList) IsAnAPIObject() {}
func (*Secret) IsAnAPIObject() {} func (*Secret) IsAnAPIObject() {}
func (*SecretList) IsAnAPIObject() {} func (*SecretList) IsAnAPIObject() {}
func (*ServiceAccount) IsAnAPIObject() {}
func (*ServiceAccountList) IsAnAPIObject() {}
func (*PersistentVolume) IsAnAPIObject() {} func (*PersistentVolume) IsAnAPIObject() {}
func (*PersistentVolumeList) IsAnAPIObject() {} func (*PersistentVolumeList) IsAnAPIObject() {}
func (*PersistentVolumeClaim) IsAnAPIObject() {} func (*PersistentVolumeClaim) IsAnAPIObject() {}
......
...@@ -816,6 +816,9 @@ type PodSpec struct { ...@@ -816,6 +816,9 @@ type PodSpec struct {
// NodeSelector is a selector which must be true for the pod to fit on a node // NodeSelector is a selector which must be true for the pod to fit on a node
NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"` NodeSelector map[string]string `json:"nodeSelector,omitempty" description:"selector which must match a node's labels for the pod to be scheduled on that node"`
// ServiceAccount is the name of the ServiceAccount to use to run this pod
ServiceAccount string `json:"serviceAccount" description:"name of the ServiceAccount to use to run this pod"`
// Host is a request to schedule this pod onto a specific host. If it is non-empty, // Host is a request to schedule this pod onto a specific host. If it is non-empty,
// the the scheduler simply schedules this pod onto that host, assuming that it fits // the the scheduler simply schedules this pod onto that host, assuming that it fits
// resource requirements. // resource requirements.
...@@ -1036,6 +1039,26 @@ type ServiceList struct { ...@@ -1036,6 +1039,26 @@ type ServiceList struct {
Items []Service `json:"items" description:"list of services"` Items []Service `json:"items" description:"list of services"`
} }
// ServiceAccount binds together:
// * a name, understood by users, and perhaps by peripheral systems, for an identity
// * a principal that can be authenticated and authorized
// * a set of secrets
type ServiceAccount struct {
TypeMeta `json:",inline"`
ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see http://docs.k8s.io/api-conventions.md#metadata"`
// Secrets is the list of secrets allowed to be used by pods running using this ServiceAccount
Secrets []ObjectReference `json:"secrets" description:"list of secrets that can be used by pods running as this service account" patchStrategy:"merge" patchMergeKey:"name"`
}
// ServiceAccountList is a list of ServiceAccount objects
type ServiceAccountList struct {
TypeMeta `json:",inline"`
ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://docs.k8s.io/api-conventions.md#metadata"`
Items []ServiceAccount `json:"items" description:"list of ServiceAccounts"`
}
// Endpoints is a collection of endpoints that implement the actual service. Example: // Endpoints is a collection of endpoints that implement the actual service. Example:
// Name: "mysvc", // Name: "mysvc",
// Subsets: [ // Subsets: [
...@@ -1708,7 +1731,23 @@ const MaxSecretSize = 1 * 1024 * 1024 ...@@ -1708,7 +1731,23 @@ const MaxSecretSize = 1 * 1024 * 1024
type SecretType string type SecretType string
const ( const (
SecretTypeOpaque SecretType = "Opaque" // Default; arbitrary user-defined data // SecretTypeOpaque is the default; arbitrary user-defined data
SecretTypeOpaque SecretType = "Opaque"
// SecretTypeServiceAccountToken contains a token that identifies a service account to the API
//
// Required fields:
// - Secret.Annotations["kubernetes.io/service-account.name"] - the name of the ServiceAccount the token identifies
// - Secret.Annotations["kubernetes.io/service-account.uid"] - the UID of the ServiceAccount the token identifies
// - Secret.Data["token"] - a token that identifies the service account to the API
SecretTypeServiceAccountToken SecretType = "kubernetes.io/service-account-token"
// ServiceAccountNameKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
ServiceAccountNameKey = "kubernetes.io/service-account.name"
// ServiceAccountUIDKey is the key of the required annotation for SecretTypeServiceAccountToken secrets
ServiceAccountUIDKey = "kubernetes.io/service-account.uid"
// ServiceAccountTokenKey is the key of the required data for SecretTypeServiceAccountToken secrets
ServiceAccountTokenKey = "token"
) )
type SecretList struct { type SecretList struct {
......
...@@ -153,6 +153,13 @@ func ValidateSecretName(name string, prefix bool) (bool, string) { ...@@ -153,6 +153,13 @@ func ValidateSecretName(name string, prefix bool) (bool, string) {
return nameIsDNSSubdomain(name, prefix) return nameIsDNSSubdomain(name, prefix)
} }
// ValidateServiceAccountName can be used to check whether the given service account name is valid.
// Prefix indicates this name will be used as part of generation, in which case
// trailing dashes are allowed.
func ValidateServiceAccountName(name string, prefix bool) (bool, string) {
return nameIsDNSSubdomain(name, prefix)
}
// ValidateEndpointsName can be used to check whether the given endpoints name is valid. // ValidateEndpointsName can be used to check whether the given endpoints name is valid.
// Prefix indicates this name will be used as part of generation, in which case // Prefix indicates this name will be used as part of generation, in which case
// trailing dashes are allowed. // trailing dashes are allowed.
...@@ -1227,6 +1234,21 @@ func ValidateLimitRange(limitRange *api.LimitRange) errs.ValidationErrorList { ...@@ -1227,6 +1234,21 @@ func ValidateLimitRange(limitRange *api.LimitRange) errs.ValidationErrorList {
return allErrs return allErrs
} }
// ValidateServiceAccount tests if required fields in the ServiceAccount are set.
func ValidateServiceAccount(serviceAccount *api.ServiceAccount) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{}
allErrs = append(allErrs, ValidateObjectMeta(&serviceAccount.ObjectMeta, true, ValidateServiceAccountName).Prefix("metadata")...)
return allErrs
}
// ValidateServiceAccountUpdate tests if required fields in the ServiceAccount are set.
func ValidateServiceAccountUpdate(oldServiceAccount, newServiceAccount *api.ServiceAccount) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{}
allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldServiceAccount.ObjectMeta, &newServiceAccount.ObjectMeta).Prefix("metadata")...)
allErrs = append(allErrs, ValidateServiceAccount(newServiceAccount)...)
return allErrs
}
// ValidateSecret tests if required fields in the Secret are set. // ValidateSecret tests if required fields in the Secret are set.
func ValidateSecret(secret *api.Secret) errs.ValidationErrorList { func ValidateSecret(secret *api.Secret) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{} allErrs := errs.ValidationErrorList{}
...@@ -1246,6 +1268,12 @@ func ValidateSecret(secret *api.Secret) errs.ValidationErrorList { ...@@ -1246,6 +1268,12 @@ func ValidateSecret(secret *api.Secret) errs.ValidationErrorList {
} }
switch secret.Type { switch secret.Type {
case api.SecretTypeServiceAccountToken:
// Only require Annotations[kubernetes.io/service-account.name]
// Additional fields (like Annotations[kubernetes.io/service-account.uid] and Data[token]) might be contributed later by a controller loop
if value := secret.Annotations[api.ServiceAccountNameKey]; len(value) == 0 {
allErrs = append(allErrs, errs.NewFieldRequired(fmt.Sprintf("metadata.annotations[%s]", api.ServiceAccountNameKey)))
}
case api.SecretTypeOpaque, "": case api.SecretTypeOpaque, "":
// no-op // no-op
default: default:
......
...@@ -2961,6 +2961,7 @@ func TestValidateNamespaceUpdate(t *testing.T) { ...@@ -2961,6 +2961,7 @@ func TestValidateNamespaceUpdate(t *testing.T) {
} }
func TestValidateSecret(t *testing.T) { func TestValidateSecret(t *testing.T) {
// Opaque secret validation
validSecret := func() api.Secret { validSecret := func() api.Secret {
return api.Secret{ return api.Secret{
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"},
...@@ -2988,6 +2989,32 @@ func TestValidateSecret(t *testing.T) { ...@@ -2988,6 +2989,32 @@ func TestValidateSecret(t *testing.T) {
} }
invalidKey.Data["a..b"] = []byte("whoops") invalidKey.Data["a..b"] = []byte("whoops")
// kubernetes.io/service-account-token secret validation
validServiceAccountTokenSecret := func() api.Secret {
return api.Secret{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: "bar",
Annotations: map[string]string{
api.ServiceAccountNameKey: "foo",
},
},
Type: api.SecretTypeServiceAccountToken,
Data: map[string][]byte{
"data-1": []byte("bar"),
},
}
}
var (
emptyTokenAnnotation = validServiceAccountTokenSecret()
missingTokenAnnotation = validServiceAccountTokenSecret()
missingTokenAnnotations = validServiceAccountTokenSecret()
)
emptyTokenAnnotation.Annotations[api.ServiceAccountNameKey] = ""
delete(missingTokenAnnotation.Annotations, api.ServiceAccountNameKey)
missingTokenAnnotations.Annotations = nil
tests := map[string]struct { tests := map[string]struct {
secret api.Secret secret api.Secret
valid bool valid bool
...@@ -2999,6 +3026,11 @@ func TestValidateSecret(t *testing.T) { ...@@ -2999,6 +3026,11 @@ func TestValidateSecret(t *testing.T) {
"invalid namespace": {invalidNs, false}, "invalid namespace": {invalidNs, false},
"over max size": {overMaxSize, false}, "over max size": {overMaxSize, false},
"invalid key": {invalidKey, false}, "invalid key": {invalidKey, false},
"valid service-account-token secret": {validServiceAccountTokenSecret(), true},
"empty service-account-token annotation": {emptyTokenAnnotation, false},
"missing service-account-token annotation": {missingTokenAnnotation, false},
"missing service-account-token annotations": {missingTokenAnnotations, false},
} }
for name, tc := range tests { for name, tc := range tests {
......
...@@ -17,8 +17,12 @@ limitations under the License. ...@@ -17,8 +17,12 @@ limitations under the License.
package apiserver package apiserver
import ( import (
"crypto/rsa"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/password/passwordfile" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/password/passwordfile"
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/basicauth" "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/basicauth"
...@@ -28,7 +32,7 @@ import ( ...@@ -28,7 +32,7 @@ import (
) )
// NewAuthenticator returns an authenticator.Request or an error // NewAuthenticator returns an authenticator.Request or an error
func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile string) (authenticator.Request, error) { func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile, serviceAccountKeyFile string, serviceAccountLookup bool, client client.Interface) (authenticator.Request, error) {
var authenticators []authenticator.Request var authenticators []authenticator.Request
if len(basicAuthFile) > 0 { if len(basicAuthFile) > 0 {
...@@ -55,6 +59,14 @@ func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile string) (authentica ...@@ -55,6 +59,14 @@ func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile string) (authentica
authenticators = append(authenticators, tokenAuth) authenticators = append(authenticators, tokenAuth)
} }
if len(serviceAccountKeyFile) > 0 {
serviceAccountAuth, err := newServiceAccountAuthenticator(serviceAccountKeyFile, serviceAccountLookup, client)
if err != nil {
return nil, err
}
authenticators = append(authenticators, serviceAccountAuth)
}
switch len(authenticators) { switch len(authenticators) {
case 0: case 0:
return nil, nil return nil, nil
...@@ -85,6 +97,16 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Request, ...@@ -85,6 +97,16 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Request,
return bearertoken.New(tokenAuthenticator), nil return bearertoken.New(tokenAuthenticator), nil
} }
// newServiceAccountAuthenticator returns an authenticator.Request or an error
func newServiceAccountAuthenticator(keyfile string, lookup bool, client client.Interface) (authenticator.Request, error) {
publicKey, err := serviceaccount.ReadPublicKey(keyfile)
if err != nil {
return nil, err
}
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]*rsa.PublicKey{publicKey}, lookup, client)
return bearertoken.New(tokenAuthenticator), nil
}
// newAuthenticatorFromClientCAFile returns an authenticator.Request or an error // newAuthenticatorFromClientCAFile returns an authenticator.Request or an error
func newAuthenticatorFromClientCAFile(clientCAFile string) (authenticator.Request, error) { func newAuthenticatorFromClientCAFile(clientCAFile string) (authenticator.Request, error) {
roots, err := util.CertPoolFromFile(clientCAFile) roots, err := util.CertPoolFromFile(clientCAFile)
......
...@@ -50,6 +50,12 @@ type Authorizer interface { ...@@ -50,6 +50,12 @@ type Authorizer interface {
Authorize(a Attributes) (err error) Authorize(a Attributes) (err error)
} }
type AuthorizerFunc func(a Attributes) error
func (f AuthorizerFunc) Authorize(a Attributes) error {
return f(a)
}
// AttributesRecord implements Attributes interface. // AttributesRecord implements Attributes interface.
type AttributesRecord struct { type AttributesRecord struct {
User user.Info User user.Info
......
...@@ -39,6 +39,7 @@ type Interface interface { ...@@ -39,6 +39,7 @@ type Interface interface {
EventNamespacer EventNamespacer
LimitRangesNamespacer LimitRangesNamespacer
ResourceQuotasNamespacer ResourceQuotasNamespacer
ServiceAccountsNamespacer
SecretsNamespacer SecretsNamespacer
NamespacesInterface NamespacesInterface
PersistentVolumesInterface PersistentVolumesInterface
...@@ -77,6 +78,10 @@ func (c *Client) ResourceQuotas(namespace string) ResourceQuotaInterface { ...@@ -77,6 +78,10 @@ func (c *Client) ResourceQuotas(namespace string) ResourceQuotaInterface {
return newResourceQuotas(c, namespace) return newResourceQuotas(c, namespace)
} }
func (c *Client) ServiceAccounts(namespace string) ServiceAccountsInterface {
return newServiceAccounts(c, namespace)
}
func (c *Client) Secrets(namespace string) SecretsInterface { func (c *Client) Secrets(namespace string) SecretsInterface {
return newSecrets(c, namespace) return newSecrets(c, namespace)
} }
......
...@@ -310,6 +310,9 @@ var fieldMappings = versionToResourceToFieldMapping{ ...@@ -310,6 +310,9 @@ var fieldMappings = versionToResourceToFieldMapping{
"secrets": clientFieldNameToAPIVersionFieldName{ "secrets": clientFieldNameToAPIVersionFieldName{
SecretType: "type", SecretType: "type",
}, },
"serviceAccounts": clientFieldNameToAPIVersionFieldName{
ObjectNameField: "name",
},
}, },
"v1beta2": resourceTypeToFieldMapping{ "v1beta2": resourceTypeToFieldMapping{
"nodes": clientFieldNameToAPIVersionFieldName{ "nodes": clientFieldNameToAPIVersionFieldName{
...@@ -326,6 +329,9 @@ var fieldMappings = versionToResourceToFieldMapping{ ...@@ -326,6 +329,9 @@ var fieldMappings = versionToResourceToFieldMapping{
"secrets": clientFieldNameToAPIVersionFieldName{ "secrets": clientFieldNameToAPIVersionFieldName{
SecretType: "type", SecretType: "type",
}, },
"serviceAccounts": clientFieldNameToAPIVersionFieldName{
ObjectNameField: "name",
},
}, },
"v1beta3": resourceTypeToFieldMapping{ "v1beta3": resourceTypeToFieldMapping{
"nodes": clientFieldNameToAPIVersionFieldName{ "nodes": clientFieldNameToAPIVersionFieldName{
...@@ -342,6 +348,9 @@ var fieldMappings = versionToResourceToFieldMapping{ ...@@ -342,6 +348,9 @@ var fieldMappings = versionToResourceToFieldMapping{
"secrets": clientFieldNameToAPIVersionFieldName{ "secrets": clientFieldNameToAPIVersionFieldName{
SecretType: "type", SecretType: "type",
}, },
"serviceAccounts": clientFieldNameToAPIVersionFieldName{
ObjectNameField: "metadata.name",
},
}, },
} }
......
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 client
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
)
type ServiceAccountsNamespacer interface {
ServiceAccounts(namespace string) ServiceAccountsInterface
}
type ServiceAccountsInterface interface {
Create(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error)
Update(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error)
Delete(name string) error
List(label labels.Selector, field fields.Selector) (*api.ServiceAccountList, error)
Get(name string) (*api.ServiceAccount, error)
Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error)
}
// serviceAccounts implements ServiceAccounts interface
type serviceAccounts struct {
client *Client
namespace string
}
// newServiceAccounts returns a new serviceAccounts object.
func newServiceAccounts(c *Client, ns string) ServiceAccountsInterface {
return &serviceAccounts{
client: c,
namespace: ns,
}
}
func (s *serviceAccounts) Create(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error) {
result := &api.ServiceAccount{}
err := s.client.Post().
Namespace(s.namespace).
Resource("serviceAccounts").
Body(serviceAccount).
Do().
Into(result)
return result, err
}
// List returns a list of serviceAccounts matching the selectors.
func (s *serviceAccounts) List(label labels.Selector, field fields.Selector) (*api.ServiceAccountList, error) {
result := &api.ServiceAccountList{}
err := s.client.Get().
Namespace(s.namespace).
Resource("serviceAccounts").
LabelsSelectorParam(label).
FieldsSelectorParam(field).
Do().
Into(result)
return result, err
}
// Get returns the given serviceAccount, or an error.
func (s *serviceAccounts) Get(name string) (*api.ServiceAccount, error) {
result := &api.ServiceAccount{}
err := s.client.Get().
Namespace(s.namespace).
Resource("serviceAccounts").
Name(name).
Do().
Into(result)
return result, err
}
// Watch starts watching for serviceAccounts matching the given selectors.
func (s *serviceAccounts) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) {
return s.client.Get().
Prefix("watch").
Namespace(s.namespace).
Resource("serviceAccounts").
Param("resourceVersion", resourceVersion).
LabelsSelectorParam(label).
FieldsSelectorParam(field).
Watch()
}
func (s *serviceAccounts) Delete(name string) error {
return s.client.Delete().
Namespace(s.namespace).
Resource("serviceAccounts").
Name(name).
Do().
Error()
}
func (s *serviceAccounts) Update(serviceAccount *api.ServiceAccount) (result *api.ServiceAccount, err error) {
result = &api.ServiceAccount{}
err = s.client.Put().
Namespace(s.namespace).
Resource("serviceAccounts").
Name(serviceAccount.Name).
Body(serviceAccount).
Do().
Into(result)
return
}
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 testclient
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
)
// FakeServiceAccounts implements ServiceAccountsInterface. Meant to be embedded into a struct to get a default
// implementation. This makes faking out just the method you want to test easier.
type FakeServiceAccounts struct {
Fake *Fake
Namespace string
}
func (c *FakeServiceAccounts) List(labels labels.Selector, field fields.Selector) (*api.ServiceAccountList, error) {
obj, err := c.Fake.Invokes(FakeAction{Action: "list-serviceaccounts"}, &api.ServiceAccountList{})
return obj.(*api.ServiceAccountList), err
}
func (c *FakeServiceAccounts) Get(name string) (*api.ServiceAccount, error) {
obj, err := c.Fake.Invokes(FakeAction{Action: "get-serviceaccount", Value: name}, &api.ServiceAccount{})
return obj.(*api.ServiceAccount), err
}
func (c *FakeServiceAccounts) Create(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error) {
obj, err := c.Fake.Invokes(FakeAction{Action: "create-serviceaccount", Value: serviceAccount}, &api.ServiceAccount{})
return obj.(*api.ServiceAccount), err
}
func (c *FakeServiceAccounts) Update(serviceAccount *api.ServiceAccount) (*api.ServiceAccount, error) {
obj, err := c.Fake.Invokes(FakeAction{Action: "update-serviceaccount", Value: serviceAccount}, &api.ServiceAccount{})
return obj.(*api.ServiceAccount), err
}
func (c *FakeServiceAccounts) Delete(name string) error {
_, err := c.Fake.Invokes(FakeAction{Action: "delete-serviceaccount", Value: name}, &api.ServiceAccount{})
return err
}
func (c *FakeServiceAccounts) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) {
c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-serviceAccounts", Value: resourceVersion})
return c.Fake.Watch, c.Fake.Err
}
...@@ -107,6 +107,10 @@ func (c *Fake) Services(namespace string) client.ServiceInterface { ...@@ -107,6 +107,10 @@ func (c *Fake) Services(namespace string) client.ServiceInterface {
return &FakeServices{Fake: c, Namespace: namespace} return &FakeServices{Fake: c, Namespace: namespace}
} }
func (c *Fake) ServiceAccounts(namespace string) client.ServiceAccountsInterface {
return &FakeServiceAccounts{Fake: c, Namespace: namespace}
}
func (c *Fake) Secrets(namespace string) client.SecretsInterface { func (c *Fake) Secrets(namespace string) client.SecretsInterface {
return &FakeSecrets{Fake: c, Namespace: namespace} return &FakeSecrets{Fake: c, Namespace: namespace}
} }
......
...@@ -228,3 +228,69 @@ func NewInformer( ...@@ -228,3 +228,69 @@ func NewInformer(
} }
return clientState, New(cfg) return clientState, New(cfg)
} }
// NewIndexerInformer returns a cache.Indexer and a controller for populating the index
// while also providing event notifications. You should only used the returned
// cache.Index for Get/List operations; Add/Modify/Deletes will cause the event
// notifications to be faulty.
//
// Parameters:
// * lw is list and watch functions for the source of the resource you want to
// be informed of.
// * objType is an object of the type that you expect to receive.
// * resyncPeriod: if non-zero, will re-list this often (you will get OnUpdate
// calls, even if nothing changed). Otherwise, re-list will be delayed as
// long as possible (until the upstream source closes the watch or times out,
// or you stop the controller).
// * h is the object you want notifications sent to.
//
func NewIndexerInformer(
lw cache.ListerWatcher,
objType runtime.Object,
resyncPeriod time.Duration,
h ResourceEventHandler,
indexers cache.Indexers,
) (cache.Indexer, *Controller) {
// This will hold the client state, as we know it.
clientState := cache.NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, indexers)
// This will hold incoming changes. Note how we pass clientState in as a
// KeyLister, that way resync operations will result in the correct set
// of update/delete deltas.
fifo := cache.NewDeltaFIFO(cache.MetaNamespaceKeyFunc, nil, clientState)
cfg := &Config{
Queue: fifo,
ListerWatcher: lw,
ObjectType: objType,
FullResyncPeriod: resyncPeriod,
RetryOnError: false,
Process: func(obj interface{}) error {
// from oldest to newest
for _, d := range obj.(cache.Deltas) {
switch d.Type {
case cache.Sync, cache.Added, cache.Updated:
if old, exists, err := clientState.Get(d.Object); err == nil && exists {
if err := clientState.Update(d.Object); err != nil {
return err
}
h.OnUpdate(old, d.Object)
} else {
if err := clientState.Add(d.Object); err != nil {
return err
}
h.OnAdd(d.Object)
}
case cache.Deleted:
if err := clientState.Delete(d.Object); err != nil {
return err
}
h.OnDelete(d.Object)
}
}
return nil
},
}
return clientState, New(cfg)
}
...@@ -64,7 +64,9 @@ func describerMap(c *client.Client) map[string]Describer { ...@@ -64,7 +64,9 @@ func describerMap(c *client.Client) map[string]Describer {
m := map[string]Describer{ m := map[string]Describer{
"Pod": &PodDescriber{c}, "Pod": &PodDescriber{c},
"ReplicationController": &ReplicationControllerDescriber{c}, "ReplicationController": &ReplicationControllerDescriber{c},
"Secret": &SecretDescriber{c},
"Service": &ServiceDescriber{c}, "Service": &ServiceDescriber{c},
"ServiceAccount": &ServiceAccountDescriber{c},
"Minion": &NodeDescriber{c}, "Minion": &NodeDescriber{c},
"Node": &NodeDescriber{c}, "Node": &NodeDescriber{c},
"LimitRange": &LimitRangeDescriber{c}, "LimitRange": &LimitRangeDescriber{c},
...@@ -421,6 +423,44 @@ func describeReplicationController(controller *api.ReplicationController, events ...@@ -421,6 +423,44 @@ func describeReplicationController(controller *api.ReplicationController, events
}) })
} }
// SecretDescriber generates information about a secret
type SecretDescriber struct {
client.Interface
}
func (d *SecretDescriber) Describe(namespace, name string) (string, error) {
c := d.Secrets(namespace)
secret, err := c.Get(name)
if err != nil {
return "", err
}
return describeSecret(secret)
}
func describeSecret(secret *api.Secret) (string, error) {
return tabbedString(func(out io.Writer) error {
fmt.Fprintf(out, "Name:\t%s\n", secret.Name)
fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(secret.Labels))
fmt.Fprintf(out, "Annotations:\t%s\n", formatLabels(secret.Annotations))
fmt.Fprintf(out, "\nType:\t%s\n", secret.Type)
fmt.Fprintf(out, "\nData\n====\n")
for k, v := range secret.Data {
switch {
case k == api.ServiceAccountTokenKey && secret.Type == api.SecretTypeServiceAccountToken:
fmt.Fprintf(out, "%s:\t%s\n", k, string(v))
default:
fmt.Fprintf(out, "%s:\t%d bytes\n", k, len(v))
}
}
return nil
})
}
// ServiceDescriber generates information about a service. // ServiceDescriber generates information about a service.
type ServiceDescriber struct { type ServiceDescriber struct {
client.Interface client.Interface
...@@ -471,6 +511,67 @@ func describeService(service *api.Service, endpoints *api.Endpoints, events *api ...@@ -471,6 +511,67 @@ func describeService(service *api.Service, endpoints *api.Endpoints, events *api
}) })
} }
// ServiceAccountDescriber generates information about a service.
type ServiceAccountDescriber struct {
client.Interface
}
func (d *ServiceAccountDescriber) Describe(namespace, name string) (string, error) {
c := d.ServiceAccounts(namespace)
serviceAccount, err := c.Get(name)
if err != nil {
return "", err
}
tokens := []api.Secret{}
tokenSelector := fields.SelectorFromSet(map[string]string{client.SecretType: string(api.SecretTypeServiceAccountToken)})
secrets, err := d.Secrets(namespace).List(labels.Everything(), tokenSelector)
if err == nil {
for _, s := range secrets.Items {
name, _ := s.Annotations[api.ServiceAccountNameKey]
uid, _ := s.Annotations[api.ServiceAccountUIDKey]
if name == serviceAccount.Name && uid == string(serviceAccount.UID) {
tokens = append(tokens, s)
}
}
}
return describeServiceAccount(serviceAccount, tokens)
}
func describeServiceAccount(serviceAccount *api.ServiceAccount, tokens []api.Secret) (string, error) {
return tabbedString(func(out io.Writer) error {
fmt.Fprintf(out, "Name:\t%s\n", serviceAccount.Name)
fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(serviceAccount.Labels))
if len(serviceAccount.Secrets) == 0 {
fmt.Fprintf(out, "Secrets:\t<none>\n")
} else {
prefix := "Secrets:"
for _, s := range serviceAccount.Secrets {
fmt.Fprintf(out, "%s\t%s\n", prefix, s)
prefix = " "
}
fmt.Fprintln(out)
}
if len(tokens) == 0 {
fmt.Fprintf(out, "Tokens: \t<none>\n")
} else {
prefix := "Tokens: "
for _, t := range tokens {
fmt.Fprintf(out, "%s\t%s\n", prefix, t.Name)
prefix = " "
}
fmt.Fprintln(out)
}
return nil
})
}
// NodeDescriber generates information about a node. // NodeDescriber generates information about a node.
type NodeDescriber struct { type NodeDescriber struct {
client.Interface client.Interface
......
...@@ -255,6 +255,7 @@ var limitRangeColumns = []string{"NAME"} ...@@ -255,6 +255,7 @@ var limitRangeColumns = []string{"NAME"}
var resourceQuotaColumns = []string{"NAME"} var resourceQuotaColumns = []string{"NAME"}
var namespaceColumns = []string{"NAME", "LABELS", "STATUS"} var namespaceColumns = []string{"NAME", "LABELS", "STATUS"}
var secretColumns = []string{"NAME", "TYPE", "DATA"} var secretColumns = []string{"NAME", "TYPE", "DATA"}
var serviceAccountColumns = []string{"NAME", "SECRETS"}
var persistentVolumeColumns = []string{"NAME", "LABELS", "CAPACITY", "ACCESSMODES", "STATUS", "CLAIM"} var persistentVolumeColumns = []string{"NAME", "LABELS", "CAPACITY", "ACCESSMODES", "STATUS", "CLAIM"}
var persistentVolumeClaimColumns = []string{"NAME", "LABELS", "STATUS", "VOLUME"} var persistentVolumeClaimColumns = []string{"NAME", "LABELS", "STATUS", "VOLUME"}
var componentStatusColumns = []string{"NAME", "STATUS", "MESSAGE", "ERROR"} var componentStatusColumns = []string{"NAME", "STATUS", "MESSAGE", "ERROR"}
...@@ -283,6 +284,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() { ...@@ -283,6 +284,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() {
h.Handler(namespaceColumns, printNamespaceList) h.Handler(namespaceColumns, printNamespaceList)
h.Handler(secretColumns, printSecret) h.Handler(secretColumns, printSecret)
h.Handler(secretColumns, printSecretList) h.Handler(secretColumns, printSecretList)
h.Handler(serviceAccountColumns, printServiceAccount)
h.Handler(serviceAccountColumns, printServiceAccountList)
h.Handler(persistentVolumeClaimColumns, printPersistentVolumeClaim) h.Handler(persistentVolumeClaimColumns, printPersistentVolumeClaim)
h.Handler(persistentVolumeClaimColumns, printPersistentVolumeClaimList) h.Handler(persistentVolumeClaimColumns, printPersistentVolumeClaimList)
h.Handler(persistentVolumeColumns, printPersistentVolume) h.Handler(persistentVolumeColumns, printPersistentVolume)
...@@ -596,6 +599,21 @@ func printSecretList(list *api.SecretList, w io.Writer) error { ...@@ -596,6 +599,21 @@ func printSecretList(list *api.SecretList, w io.Writer) error {
return nil return nil
} }
func printServiceAccount(item *api.ServiceAccount, w io.Writer) error {
_, err := fmt.Fprintf(w, "%s\t%d\n", item.Name, len(item.Secrets))
return err
}
func printServiceAccountList(list *api.ServiceAccountList, w io.Writer) error {
for _, item := range list.Items {
if err := printServiceAccount(&item, w); err != nil {
return err
}
}
return nil
}
func printNode(node *api.Node, w io.Writer) error { func printNode(node *api.Node, w io.Writer) error {
conditionMap := make(map[api.NodeConditionType]*api.NodeCondition) conditionMap := make(map[api.NodeConditionType]*api.NodeCondition)
NodeAllConditions := []api.NodeConditionType{api.NodeReady} NodeAllConditions := []api.NodeConditionType{api.NodeReady}
......
...@@ -65,6 +65,7 @@ import ( ...@@ -65,6 +65,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
ipallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator" ipallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
etcdipallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator/etcd" etcdipallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator/etcd"
serviceaccountetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/serviceaccount/etcd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
"github.com/GoogleCloudPlatform/kubernetes/pkg/ui" "github.com/GoogleCloudPlatform/kubernetes/pkg/ui"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
...@@ -402,6 +403,7 @@ func (m *Master) init(c *Config) { ...@@ -402,6 +403,7 @@ func (m *Master) init(c *Config) {
resourceQuotaStorage, resourceQuotaStatusStorage := resourcequotaetcd.NewStorage(c.EtcdHelper) resourceQuotaStorage, resourceQuotaStatusStorage := resourcequotaetcd.NewStorage(c.EtcdHelper)
secretStorage := secretetcd.NewStorage(c.EtcdHelper) secretStorage := secretetcd.NewStorage(c.EtcdHelper)
serviceAccountStorage := serviceaccountetcd.NewStorage(c.EtcdHelper)
persistentVolumeStorage, persistentVolumeStatusStorage := pvetcd.NewStorage(c.EtcdHelper) persistentVolumeStorage, persistentVolumeStatusStorage := pvetcd.NewStorage(c.EtcdHelper)
persistentVolumeClaimStorage, persistentVolumeClaimStatusStorage := pvcetcd.NewStorage(c.EtcdHelper) persistentVolumeClaimStorage, persistentVolumeClaimStatusStorage := pvcetcd.NewStorage(c.EtcdHelper)
...@@ -453,6 +455,7 @@ func (m *Master) init(c *Config) { ...@@ -453,6 +455,7 @@ func (m *Master) init(c *Config) {
"namespaces/status": namespaceStatusStorage, "namespaces/status": namespaceStatusStorage,
"namespaces/finalize": namespaceFinalizeStorage, "namespaces/finalize": namespaceFinalizeStorage,
"secrets": secretStorage, "secrets": secretStorage,
"serviceAccounts": serviceAccountStorage,
"persistentVolumes": persistentVolumeStorage, "persistentVolumes": persistentVolumeStorage,
"persistentVolumes/status": persistentVolumeStatusStorage, "persistentVolumes/status": persistentVolumeStatusStorage,
"persistentVolumeClaims": persistentVolumeClaimStorage, "persistentVolumeClaims": persistentVolumeClaimStorage,
......
...@@ -107,6 +107,10 @@ func finalize(kubeClient client.Interface, namespace api.Namespace) (*api.Namesp ...@@ -107,6 +107,10 @@ func finalize(kubeClient client.Interface, namespace api.Namespace) (*api.Namesp
// deleteAllContent will delete all content known to the system in a namespace // deleteAllContent will delete all content known to the system in a namespace
func deleteAllContent(kubeClient client.Interface, namespace string) (err error) { func deleteAllContent(kubeClient client.Interface, namespace string) (err error) {
err = deleteServiceAccounts(kubeClient, namespace)
if err != nil {
return err
}
err = deleteServices(kubeClient, namespace) err = deleteServices(kubeClient, namespace)
if err != nil { if err != nil {
return err return err
...@@ -217,6 +221,20 @@ func deleteResourceQuotas(kubeClient client.Interface, ns string) error { ...@@ -217,6 +221,20 @@ func deleteResourceQuotas(kubeClient client.Interface, ns string) error {
return nil return nil
} }
func deleteServiceAccounts(kubeClient client.Interface, ns string) error {
items, err := kubeClient.ServiceAccounts(ns).List(labels.Everything(), fields.Everything())
if err != nil {
return err
}
for i := range items.Items {
err := kubeClient.ServiceAccounts(ns).Delete(items.Items[i].Name)
if err != nil {
return err
}
}
return nil
}
func deleteServices(kubeClient client.Interface, ns string) error { func deleteServices(kubeClient client.Interface, ns string) error {
items, err := kubeClient.Services(ns).List(labels.Everything()) items, err := kubeClient.Services(ns).List(labels.Everything())
if err != nil { if err != nil {
......
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 serviceaccount provides a Registry interface and a strategy
// implementation for storing ServiceAccount API objects.
package serviceaccount
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 etcd
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic"
etcdgeneric "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic/etcd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/serviceaccount"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
)
// REST implements a RESTStorage for service accounts against etcd
type REST struct {
*etcdgeneric.Etcd
}
const Prefix = "/serviceaccounts"
// NewStorage returns a RESTStorage object that will work against service accounts objects.
func NewStorage(h tools.EtcdHelper) *REST {
store := &etcdgeneric.Etcd{
NewFunc: func() runtime.Object { return &api.ServiceAccount{} },
NewListFunc: func() runtime.Object { return &api.ServiceAccountList{} },
KeyRootFunc: func(ctx api.Context) string {
return etcdgeneric.NamespaceKeyRootFunc(ctx, Prefix)
},
KeyFunc: func(ctx api.Context, name string) (string, error) {
return etcdgeneric.NamespaceKeyFunc(ctx, Prefix, name)
},
ObjectNameFunc: func(obj runtime.Object) (string, error) {
return obj.(*api.ServiceAccount).Name, nil
},
PredicateFunc: func(label labels.Selector, field fields.Selector) generic.Matcher {
return serviceaccount.Matcher(label, field)
},
EndpointName: "serviceaccounts",
Helper: h,
}
store.CreateStrategy = serviceaccount.Strategy
store.UpdateStrategy = serviceaccount.Strategy
store.ReturnDeletedObject = true
return &REST{store}
}
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 etcd
import (
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools/etcdtest"
)
func newHelper(t *testing.T) (*tools.FakeEtcdClient, tools.EtcdHelper) {
fakeEtcdClient := tools.NewFakeEtcdClient(t)
fakeEtcdClient.TestIndex = true
helper := tools.NewEtcdHelper(fakeEtcdClient, testapi.Codec(), etcdtest.PathPrefix())
return fakeEtcdClient, helper
}
func validNewServiceAccount(name string) *api.ServiceAccount {
return &api.ServiceAccount{
ObjectMeta: api.ObjectMeta{
Name: name,
Namespace: api.NamespaceDefault,
},
Secrets: []api.ObjectReference{},
}
}
func TestCreate(t *testing.T) {
fakeEtcdClient, helper := newHelper(t)
storage := NewStorage(helper)
test := resttest.New(t, storage, fakeEtcdClient.SetError)
serviceAccount := validNewServiceAccount("foo")
serviceAccount.Name = ""
serviceAccount.GenerateName = "foo-"
test.TestCreate(
// valid
serviceAccount,
// invalid
&api.ServiceAccount{},
&api.ServiceAccount{
ObjectMeta: api.ObjectMeta{Name: "name with spaces"},
},
)
}
func TestUpdate(t *testing.T) {
fakeEtcdClient, helper := newHelper(t)
storage := NewStorage(helper)
test := resttest.New(t, storage, fakeEtcdClient.SetError)
key := etcdtest.AddPrefix("serviceaccounts/default/foo")
fakeEtcdClient.ExpectNotFoundGet(key)
fakeEtcdClient.ChangeIndex = 2
serviceAccount := validNewServiceAccount("foo")
existing := validNewServiceAccount("exists")
obj, err := storage.Create(api.NewDefaultContext(), existing)
if err != nil {
t.Fatalf("unable to create object: %v", err)
}
older := obj.(*api.ServiceAccount)
older.ResourceVersion = "1"
test.TestUpdate(
serviceAccount,
existing,
older,
)
}
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 serviceaccount
import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
)
// Registry is an interface implemented by things that know how to store ServiceAccount objects.
type Registry interface {
// ListServiceAccounts obtains a list of ServiceAccounts having labels which match selector.
ListServiceAccounts(ctx api.Context, selector labels.Selector) (*api.ServiceAccountList, error)
// Watch for new/changed/deleted service accounts
WatchServiceAccounts(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error)
// Get a specific ServiceAccount
GetServiceAccount(ctx api.Context, name string) (*api.ServiceAccount, error)
// Create a ServiceAccount based on a specification.
CreateServiceAccount(ctx api.Context, ServiceAccount *api.ServiceAccount) error
// Update an existing ServiceAccount
UpdateServiceAccount(ctx api.Context, ServiceAccount *api.ServiceAccount) error
// Delete an existing ServiceAccount
DeleteServiceAccount(ctx api.Context, name string) error
}
// storage puts strong typing around storage calls
type storage struct {
rest.StandardStorage
}
// NewRegistry returns a new Registry interface for the given Storage. Any mismatched
// types will panic.
func NewRegistry(s rest.StandardStorage) Registry {
return &storage{s}
}
func (s *storage) ListServiceAccounts(ctx api.Context, label labels.Selector) (*api.ServiceAccountList, error) {
obj, err := s.List(ctx, label, fields.Everything())
if err != nil {
return nil, err
}
return obj.(*api.ServiceAccountList), nil
}
func (s *storage) WatchServiceAccounts(ctx api.Context, label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) {
return s.Watch(ctx, label, field, resourceVersion)
}
func (s *storage) GetServiceAccount(ctx api.Context, name string) (*api.ServiceAccount, error) {
obj, err := s.Get(ctx, name)
if err != nil {
return nil, err
}
return obj.(*api.ServiceAccount), nil
}
func (s *storage) CreateServiceAccount(ctx api.Context, serviceAccount *api.ServiceAccount) error {
_, err := s.Create(ctx, serviceAccount)
return err
}
func (s *storage) UpdateServiceAccount(ctx api.Context, serviceAccount *api.ServiceAccount) error {
_, _, err := s.Update(ctx, serviceAccount)
return err
}
func (s *storage) DeleteServiceAccount(ctx api.Context, name string) error {
_, err := s.Delete(ctx, name, nil)
return err
}
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 serviceaccount
import (
"fmt"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/generic"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors"
)
// strategy implements behavior for ServiceAccount objects
type strategy struct {
runtime.ObjectTyper
api.NameGenerator
}
// Strategy is the default logic that applies when creating and updating ServiceAccount
// objects via the REST API.
var Strategy = strategy{api.Scheme, api.SimpleNameGenerator}
func (strategy) NamespaceScoped() bool {
return true
}
func (strategy) PrepareForCreate(obj runtime.Object) {
cleanSecretReferences(obj.(*api.ServiceAccount))
}
func (strategy) Validate(ctx api.Context, obj runtime.Object) fielderrors.ValidationErrorList {
return validation.ValidateServiceAccount(obj.(*api.ServiceAccount))
}
func (strategy) AllowCreateOnUpdate() bool {
return false
}
func (strategy) PrepareForUpdate(obj, old runtime.Object) {
cleanSecretReferences(obj.(*api.ServiceAccount))
}
func cleanSecretReferences(serviceAccount *api.ServiceAccount) {
for i, secret := range serviceAccount.Secrets {
serviceAccount.Secrets[i] = api.ObjectReference{Name: secret.Name}
}
}
func (strategy) ValidateUpdate(ctx api.Context, obj, old runtime.Object) fielderrors.ValidationErrorList {
return validation.ValidateServiceAccountUpdate(old.(*api.ServiceAccount), obj.(*api.ServiceAccount))
}
// Matcher returns a generic matcher for a given label and field selector.
func Matcher(label labels.Selector, field fields.Selector) generic.Matcher {
return generic.MatcherFunc(func(obj runtime.Object) (bool, error) {
sa, ok := obj.(*api.ServiceAccount)
if !ok {
return false, fmt.Errorf("not a serviceaccount")
}
fields := SelectableFields(sa)
return label.Matches(labels.Set(sa.Labels)) && field.Matches(fields), nil
})
}
// SelectableFields returns a label set that represents the object
func SelectableFields(obj *api.ServiceAccount) labels.Set {
return labels.Set{
"metadata.name": obj.Name,
}
}
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 serviceaccount provides implementations
// to manage service accounts and service account tokens
package serviceaccount
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 serviceaccount
import (
"bytes"
"crypto/rsa"
"errors"
"fmt"
"io/ioutil"
"strings"
jwt "github.com/dgrijalva/jwt-go"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
)
const (
ServiceAccountUsernamePrefix = "serviceaccount"
ServiceAccountUsernameSeparator = ":"
Issuer = "kubernetes/serviceaccount"
SubjectClaim = "sub"
IssuerClaim = "iss"
ServiceAccountNameClaim = "kubernetes.io/serviceaccount/service-account.name"
ServiceAccountUIDClaim = "kubernetes.io/serviceaccount/service-account.uid"
SecretNameClaim = "kubernetes.io/serviceaccount/secret.name"
NamespaceClaim = "kubernetes.io/serviceaccount/namespace"
)
type TokenGenerator interface {
// GenerateToken generates a token which will identify the given ServiceAccount.
// The returned token will be stored in the given (and yet-unpersisted) Secret.
GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error)
}
// ReadPrivateKey is a helper function for reading an rsa.PrivateKey from a PEM-encoded file
func ReadPrivateKey(file string) (*rsa.PrivateKey, error) {
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
return jwt.ParseRSAPrivateKeyFromPEM(data)
}
// ReadPublicKey is a helper function for reading an rsa.PublicKey from a PEM-encoded file
// Reads public keys from both public and private key files
func ReadPublicKey(file string) (*rsa.PublicKey, error) {
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(data); err == nil {
return &privateKey.PublicKey, nil
}
return jwt.ParseRSAPublicKeyFromPEM(data)
}
// MakeUsername generates a username from the given namespace and ServiceAccount name.
// The resulting username can be passed to SplitUsername to extract the original namespace and ServiceAccount name.
func MakeUsername(namespace, name string) string {
return strings.Join([]string{ServiceAccountUsernamePrefix, namespace, name}, ServiceAccountUsernameSeparator)
}
// SplitUsername returns the namespace and ServiceAccount name embedded in the given username,
// or an error if the username is not a valid name produced by MakeUsername
func SplitUsername(username string) (string, string, error) {
parts := strings.Split(username, ServiceAccountUsernameSeparator)
if len(parts) != 3 || parts[0] != ServiceAccountUsernamePrefix || len(parts[1]) == 0 || len(parts[2]) == 0 {
return "", "", fmt.Errorf("Username must be in the form %s", MakeUsername("namespace", "name"))
}
return parts[1], parts[2], nil
}
// JWTTokenGenerator returns a TokenGenerator that generates signed JWT tokens, using the given privateKey.
// privateKey is a PEM-encoded byte array of a private RSA key.
// JWTTokenAuthenticator()
func JWTTokenGenerator(key *rsa.PrivateKey) TokenGenerator {
return &jwtTokenGenerator{key}
}
type jwtTokenGenerator struct {
key *rsa.PrivateKey
}
func (j *jwtTokenGenerator) GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) {
token := jwt.New(jwt.SigningMethodRS256)
// Identify the issuer
token.Claims[IssuerClaim] = Issuer
// Username: `serviceaccount:<namespace>:<serviceaccount>`
token.Claims[SubjectClaim] = MakeUsername(serviceAccount.Namespace, serviceAccount.Name)
// Persist enough structured info for the authenticator to be able to look up the service account and secret
token.Claims[NamespaceClaim] = serviceAccount.Namespace
token.Claims[ServiceAccountNameClaim] = serviceAccount.Name
token.Claims[ServiceAccountUIDClaim] = serviceAccount.UID
token.Claims[SecretNameClaim] = secret.Name
// Sign and get the complete encoded token as a string
return token.SignedString(j.key)
}
// JWTTokenAuthenticator authenticates tokens as JWT tokens produced by JWTTokenGenerator
// Token signatures are verified using each of the given public keys until one works (allowing key rotation)
// If lookup is true, the service account and secret referenced as claims inside the token are retrieved and verified using the given client
func JWTTokenAuthenticator(keys []*rsa.PublicKey, lookup bool, client client.Interface) authenticator.Token {
return &jwtTokenAuthenticator{keys, lookup, client}
}
type jwtTokenAuthenticator struct {
keys []*rsa.PublicKey
lookup bool
client client.Interface
}
func (j *jwtTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool, error) {
var validationError error
for _, key := range j.keys {
// Attempt to verify with each key until we find one that works
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return key, nil
})
if err != nil {
switch err := err.(type) {
case *jwt.ValidationError:
if (err.Errors & jwt.ValidationErrorMalformed) != 0 {
// Not a JWT, no point in continuing
return nil, false, nil
}
if (err.Errors & jwt.ValidationErrorSignatureInvalid) != 0 {
// Signature error, perhaps one of the other keys will verify the signature
// If not, we want to return this error
validationError = err
continue
}
}
// Other errors should just return as errors
return nil, false, err
}
// If we get here, we have a token with a recognized signature
// Make sure we issued the token
iss, _ := parsedToken.Claims[IssuerClaim].(string)
if iss != Issuer {
return nil, false, nil
}
// Make sure the claims we need exist
sub, _ := parsedToken.Claims[SubjectClaim].(string)
if len(sub) == 0 {
return nil, false, errors.New("sub claim is missing")
}
namespace, _ := parsedToken.Claims[NamespaceClaim].(string)
if len(namespace) == 0 {
return nil, false, errors.New("namespace claim is missing")
}
secretName, _ := parsedToken.Claims[SecretNameClaim].(string)
if len(namespace) == 0 {
return nil, false, errors.New("secretName claim is missing")
}
serviceAccountName, _ := parsedToken.Claims[ServiceAccountNameClaim].(string)
if len(serviceAccountName) == 0 {
return nil, false, errors.New("serviceAccountName claim is missing")
}
serviceAccountUID, _ := parsedToken.Claims[ServiceAccountUIDClaim].(string)
if len(serviceAccountUID) == 0 {
return nil, false, errors.New("serviceAccountUID claim is missing")
}
if j.lookup {
// Make sure token hasn't been invalidated by deletion of the secret
secret, err := j.client.Secrets(namespace).Get(secretName)
if err != nil {
return nil, false, errors.New("Token has been invalidated")
}
if bytes.Compare(secret.Data[api.ServiceAccountTokenKey], []byte(token)) != 0 {
return nil, false, errors.New("Token does not match server's copy")
}
// Make sure service account still exists (name and UID)
serviceAccount, err := j.client.ServiceAccounts(namespace).Get(serviceAccountName)
if err != nil {
return nil, false, err
}
if string(serviceAccount.UID) != serviceAccountUID {
return nil, false, fmt.Errorf("ServiceAccount UID (%s) does not match claim (%s)", serviceAccount.UID, serviceAccountUID)
}
}
return &user.DefaultInfo{
Name: sub,
UID: serviceAccountUID,
Groups: []string{},
}, true, nil
}
return nil, false, validationError
}
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 serviceaccount
import (
"crypto/rsa"
"io/ioutil"
"os"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
"github.com/dgrijalva/jwt-go"
)
const otherPublicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArXz0QkIG1B5Bj2/W69GH
rsm5e+RC3kE+VTgocge0atqlLBek35tRqLgUi3AcIrBZ/0YctMSWDVcRt5fkhWwe
Lqjj6qvAyNyOkrkBi1NFDpJBjYJtuKHgRhNxXbOzTSNpdSKXTfOkzqv56MwHOP25
yP/NNAODUtr92D5ySI5QX8RbXW+uDn+ixul286PBW/BCrE4tuS88dA0tYJPf8LCu
sqQOwlXYH/rNUg4Pyl9xxhR5DIJR0OzNNfChjw60zieRIt2LfM83fXhwk8IxRGkc
gPZm7ZsipmfbZK2Tkhnpsa4QxDg7zHJPMsB5kxRXW0cQipXcC3baDyN9KBApNXa0
PwIDAQAB
-----END PUBLIC KEY-----`
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA249XwEo9k4tM8fMxV7zx
OhcrP+WvXn917koM5Qr2ZXs4vo26e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecI
zshKuv1gKIxbbLQMOuK1eA/4HALyEkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG
51RoiMgbQxaCyYxGfNLpLAZK9L0Tctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJU
j7OTh/AjjCnMnkgvKT2tpKxYQ59PgDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEi
B4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRM
WwIDAQAB
-----END PUBLIC KEY-----
`
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA249XwEo9k4tM8fMxV7zxOhcrP+WvXn917koM5Qr2ZXs4vo26
e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecIzshKuv1gKIxbbLQMOuK1eA/4HALy
EkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG51RoiMgbQxaCyYxGfNLpLAZK9L0T
ctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJUj7OTh/AjjCnMnkgvKT2tpKxYQ59P
gDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEiB4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp
1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRMWwIDAQABAoIBAHJx8GqyCBDNbqk7
e7/hI9iE1S10Wwol5GH2RWxqX28cYMKq+8aE2LI1vPiXO89xOgelk4DN6urX6xjK
ZBF8RRIMQy/e/O2F4+3wl+Nl4vOXV1u6iVXMsD6JRg137mqJf1Fr9elg1bsaRofL
Q7CxPoB8dhS+Qb+hj0DhlqhgA9zG345CQCAds0ZYAZe8fP7bkwrLqZpMn7Dz9WVm
++YgYYKjuE95kPuup/LtWfA9rJyE/Fws8/jGvRSpVn1XglMLSMKhLd27sE8ZUSV0
2KUzbfRGE0+AnRULRrjpYaPu0XQ2JjdNvtkjBnv27RB89W9Gklxq821eH1Y8got8
FZodjxECgYEA93pz7AQZ2xDs67d1XLCzpX84GxKzttirmyj3OIlxgzVHjEMsvw8v
sjFiBU5xEEQDosrBdSknnlJqyiq1YwWG/WDckr13d8G2RQWoySN7JVmTQfXcLoTu
YGRiiTuoEi3ab3ZqrgGrFgX7T/cHuasbYvzCvhM2b4VIR3aSxU2DTUMCgYEA4x7J
T/ErP6GkU5nKstu/mIXwNzayEO1BJvPYsy7i7EsxTm3xe/b8/6cYOz5fvJLGH5mT
Q8YvuLqBcMwZardrYcwokD55UvNLOyfADDFZ6l3WntIqbA640Ok2g1X4U8J09xIq
ZLIWK1yWbbvi4QCeN5hvWq47e8sIj5QHjIIjRwkCgYEAyNqjltxFN9zmzPDa2d24
EAvOt3pYTYBQ1t9KtqImdL0bUqV6fZ6PsWoPCgt+DBuHb+prVPGP7Bkr/uTmznU/
+AlTO+12NsYLbr2HHagkXE31DEXE7CSLa8RNjN/UKtz4Ohq7vnowJvG35FCz/mb3
FUHbtHTXa2+bGBUOTf/5Hw0CgYBxw0r9EwUhw1qnUYJ5op7OzFAtp+T7m4ul8kCa
SCL8TxGsgl+SQ34opE775dtYfoBk9a0RJqVit3D8yg71KFjOTNAIqHJm/Vyyjc+h
i9rJDSXiuczsAVfLtPVMRfS0J9QkqeG4PIfkQmVLI/CZ2ZBmsqEcX+eFs4ZfPLun
Qsxe2QKBgGuPilIbLeIBDIaPiUI0FwU8v2j8CEQBYvoQn34c95hVQsig/o5z7zlo
UsO0wlTngXKlWdOcCs1kqEhTLrstf48djDxAYAxkw40nzeJOt7q52ib/fvf4/UBy
X024wzbiw1q07jFCyfQmODzURAx1VNT7QVUMdz/N8vy47/H40AZJ
-----END RSA PRIVATE KEY-----
`
func getPrivateKey(data string) *rsa.PrivateKey {
key, _ := jwt.ParseRSAPrivateKeyFromPEM([]byte(data))
return key
}
func getPublicKey(data string) *rsa.PublicKey {
key, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(data))
return key
}
func TestReadPrivateKey(t *testing.T) {
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
defer os.Remove(f.Name())
if err := ioutil.WriteFile(f.Name(), []byte(privateKey), os.FileMode(0600)); err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
if _, err := ReadPrivateKey(f.Name()); err != nil {
t.Fatalf("error reading key: %v", err)
}
}
func TestReadPublicKey(t *testing.T) {
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
defer os.Remove(f.Name())
if err := ioutil.WriteFile(f.Name(), []byte(publicKey), os.FileMode(0600)); err != nil {
t.Fatalf("error creating tmpfile: %v", err)
}
if _, err := ReadPublicKey(f.Name()); err != nil {
t.Fatalf("error reading key: %v", err)
}
}
func TestTokenGenerateAndValidate(t *testing.T) {
expectedUserName := "serviceaccount:test:my-service-account"
expectedUserUID := "12345"
// Related API objects
serviceAccount := &api.ServiceAccount{
ObjectMeta: api.ObjectMeta{
Name: "my-service-account",
UID: "12345",
Namespace: "test",
},
}
secret := &api.Secret{
ObjectMeta: api.ObjectMeta{
Name: "my-secret",
Namespace: "test",
},
}
// Generate the token
generator := JWTTokenGenerator(getPrivateKey(privateKey))
token, err := generator.GenerateToken(*serviceAccount, *secret)
if err != nil {
t.Fatalf("error generating token: %v", err)
}
if len(token) == 0 {
t.Fatalf("no token generated")
}
// "Save" the token
secret.Data = map[string][]byte{
"token": []byte(token),
}
testCases := map[string]struct {
Client client.Interface
Keys []*rsa.PublicKey
ExpectedErr bool
ExpectedOK bool
ExpectedUserName string
ExpectedUserUID string
}{
"no keys": {
Client: nil,
Keys: []*rsa.PublicKey{},
ExpectedErr: false,
ExpectedOK: false,
},
"invalid keys": {
Client: nil,
Keys: []*rsa.PublicKey{getPublicKey(otherPublicKey)},
ExpectedErr: true,
ExpectedOK: false,
},
"valid key": {
Client: nil,
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
ExpectedErr: false,
ExpectedOK: true,
ExpectedUserName: expectedUserName,
ExpectedUserUID: expectedUserUID,
},
"rotated keys": {
Client: nil,
Keys: []*rsa.PublicKey{getPublicKey(otherPublicKey), getPublicKey(publicKey)},
ExpectedErr: false,
ExpectedOK: true,
ExpectedUserName: expectedUserName,
ExpectedUserUID: expectedUserUID,
},
"valid lookup": {
Client: testclient.NewSimpleFake(serviceAccount, secret),
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
ExpectedErr: false,
ExpectedOK: true,
ExpectedUserName: expectedUserName,
ExpectedUserUID: expectedUserUID,
},
"invalid secret lookup": {
Client: testclient.NewSimpleFake(serviceAccount),
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
ExpectedErr: true,
ExpectedOK: false,
},
"invalid serviceaccount lookup": {
Client: testclient.NewSimpleFake(secret),
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
ExpectedErr: true,
ExpectedOK: false,
},
}
for k, tc := range testCases {
authenticator := JWTTokenAuthenticator(tc.Keys, tc.Client != nil, tc.Client)
user, ok, err := authenticator.AuthenticateToken(token)
if (err != nil) != tc.ExpectedErr {
t.Errorf("%s: Expected error=%v, got %v", k, tc.ExpectedErr, err)
continue
}
if ok != tc.ExpectedOK {
t.Errorf("%s: Expected ok=%v, got %v", k, tc.ExpectedOK, ok)
continue
}
if err != nil || !ok {
continue
}
if user.GetName() != tc.ExpectedUserName {
t.Errorf("%s: Expected username=%v, got %v", k, tc.ExpectedUserName, user.GetName())
continue
}
if user.GetUID() != tc.ExpectedUserUID {
t.Errorf("%s: Expected userUID=%v, got %v", k, tc.ExpectedUserUID, user.GetUID())
continue
}
}
}
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 serviceaccount
import (
"fmt"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
"github.com/GoogleCloudPlatform/kubernetes/pkg/controller/framework"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
"github.com/golang/glog"
)
// nameIndexFunc is an index function that indexes based on an object's name
func nameIndexFunc(obj interface{}) (string, error) {
meta, err := meta.Accessor(obj)
if err != nil {
return "", fmt.Errorf("object has no meta: %v", err)
}
return meta.Name(), nil
}
type ServiceAccountControllerOptions struct {
Name string
ServiceAccountResync time.Duration
NamespaceResync time.Duration
}
func DefaultServiceAccountControllerOptions() ServiceAccountControllerOptions {
return ServiceAccountControllerOptions{Name: "default"}
}
// NewServiceAccountsController returns a new *ServiceAccountsController.
func NewServiceAccountsController(cl *client.Client, options ServiceAccountControllerOptions) *ServiceAccountsController {
e := &ServiceAccountsController{
client: cl,
name: options.Name,
}
accountSelector := fields.SelectorFromSet(map[string]string{client.ObjectNameField: options.Name})
e.serviceAccounts, e.serviceAccountController = framework.NewIndexerInformer(
&cache.ListWatch{
ListFunc: func() (runtime.Object, error) {
return e.client.ServiceAccounts(api.NamespaceAll).List(labels.Everything(), accountSelector)
},
WatchFunc: func(rv string) (watch.Interface, error) {
return e.client.ServiceAccounts(api.NamespaceAll).Watch(labels.Everything(), accountSelector, rv)
},
},
&api.ServiceAccount{},
options.ServiceAccountResync,
framework.ResourceEventHandlerFuncs{
DeleteFunc: e.serviceAccountDeleted,
},
cache.Indexers{"namespace": cache.MetaNamespaceIndexFunc},
)
e.namespaces, e.namespaceController = framework.NewIndexerInformer(
&cache.ListWatch{
ListFunc: func() (runtime.Object, error) {
return e.client.Namespaces().List(labels.Everything(), fields.Everything())
},
WatchFunc: func(rv string) (watch.Interface, error) {
return e.client.Namespaces().Watch(labels.Everything(), fields.Everything(), rv)
},
},
&api.Namespace{},
options.NamespaceResync,
framework.ResourceEventHandlerFuncs{
AddFunc: e.namespaceAdded,
UpdateFunc: e.namespaceUpdated,
},
cache.Indexers{"name": nameIndexFunc},
)
return e
}
// ServiceAccountsController manages ServiceAccount objects inside Namespaces
type ServiceAccountsController struct {
stopChan chan struct{}
client *client.Client
name string
serviceAccounts cache.Indexer
namespaces cache.Indexer
// Since we join two objects, we'll watch both of them with controllers.
serviceAccountController *framework.Controller
namespaceController *framework.Controller
}
// Runs controller loops and returns immediately
func (e *ServiceAccountsController) Run() {
if e.stopChan == nil {
e.stopChan = make(chan struct{})
go e.serviceAccountController.Run(e.stopChan)
go e.namespaceController.Run(e.stopChan)
}
}
// Stop gracefully shuts down this controller
func (e *ServiceAccountsController) Stop() {
if e.stopChan != nil {
close(e.stopChan)
e.stopChan = nil
}
}
// serviceAccountDeleted reacts to a ServiceAccount deletion by recreating a default ServiceAccount in the namespace if needed
func (e *ServiceAccountsController) serviceAccountDeleted(obj interface{}) {
serviceAccount, ok := obj.(*api.ServiceAccount)
if !ok {
// Unknown type. If we missed a ServiceAccount deletion, the
// corresponding secrets will be cleaned up during the Secret re-list
return
}
e.createDefaultServiceAccountIfNeeded(serviceAccount.Namespace)
}
// namespaceAdded reacts to a Namespace creation by creating a default ServiceAccount object
func (e *ServiceAccountsController) namespaceAdded(obj interface{}) {
namespace := obj.(*api.Namespace)
e.createDefaultServiceAccountIfNeeded(namespace.Name)
}
// namespaceUpdated reacts to a Namespace update (or re-list) by creating a default ServiceAccount in the namespace if needed
func (e *ServiceAccountsController) namespaceUpdated(oldObj interface{}, newObj interface{}) {
newNamespace := newObj.(*api.Namespace)
e.createDefaultServiceAccountIfNeeded(newNamespace.Name)
}
// createDefaultServiceAccountIfNeeded creates a default ServiceAccount in the given namespace if:
// * it default ServiceAccount does not already exist
// * the specified namespace exists
// * the specified namespace is in the ACTIVE phase
func (e *ServiceAccountsController) createDefaultServiceAccountIfNeeded(namespace string) {
serviceAccount, err := e.getDefaultServiceAccount(namespace)
if err != nil {
glog.Error(err)
return
}
if serviceAccount != nil {
// If service account already exists, it doesn't need to be created
return
}
namespaceObj, err := e.getNamespace(namespace)
if err != nil {
glog.Error(err)
return
}
if namespaceObj == nil {
// If namespace does not exist, no service account is needed
return
}
if namespaceObj.Status.Phase != api.NamespaceActive {
// If namespace is not active, we shouldn't try to create anything
return
}
e.createDefaultServiceAccount(namespace)
}
// createDefaultServiceAccount creates a default ServiceAccount in the specified namespace
func (e *ServiceAccountsController) createDefaultServiceAccount(namespace string) {
serviceAccount := &api.ServiceAccount{}
serviceAccount.Name = e.name
serviceAccount.Namespace = namespace
if _, err := e.client.ServiceAccounts(namespace).Create(serviceAccount); err != nil {
glog.Error(err)
}
}
// getDefaultServiceAccount returns the default ServiceAccount for the given namespace
func (e *ServiceAccountsController) getDefaultServiceAccount(namespace string) (*api.ServiceAccount, error) {
key := &api.ServiceAccount{ObjectMeta: api.ObjectMeta{Namespace: namespace}}
accounts, err := e.serviceAccounts.Index("namespace", key)
if err != nil {
return nil, err
}
for _, obj := range accounts {
serviceAccount := obj.(*api.ServiceAccount)
if e.name == serviceAccount.Name {
return serviceAccount, nil
}
}
return nil, nil
}
// getNamespace returns the Namespace with the given name
func (e *ServiceAccountsController) getNamespace(name string) (*api.Namespace, error) {
key := &api.Namespace{ObjectMeta: api.ObjectMeta{Name: name}}
namespaces, err := e.namespaces.Index("name", key)
if err != nil {
return nil, err
}
if len(namespaces) == 0 {
return nil, nil
}
if len(namespaces) == 1 {
return namespaces[0].(*api.Namespace), nil
}
return nil, fmt.Errorf("%d namespaces with the name %s indexed", len(namespaces), name)
}
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 serviceaccount
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
type serverResponse struct {
statusCode int
obj interface{}
}
func makeTestServer(t *testing.T, namespace string, serviceAccountResponse serverResponse) (*httptest.Server, *util.FakeHandler) {
fakeServiceAccountsHandler := util.FakeHandler{
StatusCode: serviceAccountResponse.statusCode,
ResponseBody: runtime.EncodeOrDie(testapi.Codec(), serviceAccountResponse.obj.(runtime.Object)),
}
mux := http.NewServeMux()
mux.Handle(testapi.ResourcePath("serviceAccounts", namespace, ""), &fakeServiceAccountsHandler)
mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
t.Errorf("unexpected request: %v", req.RequestURI)
res.WriteHeader(http.StatusNotFound)
})
return httptest.NewServer(mux), &fakeServiceAccountsHandler
}
func TestServiceAccountCreation(t *testing.T) {
ns := api.NamespaceDefault
activeNS := &api.Namespace{
ObjectMeta: api.ObjectMeta{Name: ns},
Status: api.NamespaceStatus{
Phase: api.NamespaceActive,
},
}
terminatingNS := &api.Namespace{
ObjectMeta: api.ObjectMeta{Name: ns},
Status: api.NamespaceStatus{
Phase: api.NamespaceTerminating,
},
}
serviceAccount := &api.ServiceAccount{
ObjectMeta: api.ObjectMeta{
Name: "default",
Namespace: ns,
ResourceVersion: "1",
},
}
testcases := map[string]struct {
ExistingNamespace *api.Namespace
ExistingServiceAccount *api.ServiceAccount
AddedNamespace *api.Namespace
UpdatedNamespace *api.Namespace
DeletedServiceAccount *api.ServiceAccount
ExpectCreatedServiceAccount bool
}{
"new active namespace missing serviceaccount": {
AddedNamespace: activeNS,
ExpectCreatedServiceAccount: true,
},
"new active namespace with serviceaccount": {
ExistingServiceAccount: serviceAccount,
AddedNamespace: activeNS,
ExpectCreatedServiceAccount: false,
},
"new terminating namespace": {
AddedNamespace: terminatingNS,
ExpectCreatedServiceAccount: false,
},
"updated active namespace missing serviceaccount": {
UpdatedNamespace: activeNS,
ExpectCreatedServiceAccount: true,
},
"updated active namespace with serviceaccount": {
ExistingServiceAccount: serviceAccount,
UpdatedNamespace: activeNS,
ExpectCreatedServiceAccount: false,
},
"updated terminating namespace": {
UpdatedNamespace: terminatingNS,
ExpectCreatedServiceAccount: false,
},
"deleted serviceaccount without namespace": {
DeletedServiceAccount: serviceAccount,
ExpectCreatedServiceAccount: false,
},
"deleted serviceaccount with active namespace": {
ExistingNamespace: activeNS,
DeletedServiceAccount: serviceAccount,
ExpectCreatedServiceAccount: true,
},
"deleted serviceaccount with terminating namespace": {
ExistingNamespace: terminatingNS,
DeletedServiceAccount: serviceAccount,
ExpectCreatedServiceAccount: false,
},
}
for k, tc := range testcases {
testServer, handler := makeTestServer(t, ns, serverResponse{http.StatusOK, serviceAccount})
client := client.NewOrDie(&client.Config{Host: testServer.URL, Version: testapi.Version()})
controller := NewServiceAccountsController(client, DefaultServiceAccountControllerOptions())
if tc.ExistingNamespace != nil {
controller.namespaces.Add(tc.ExistingNamespace)
}
if tc.ExistingServiceAccount != nil {
controller.serviceAccounts.Add(tc.ExistingServiceAccount)
}
if tc.AddedNamespace != nil {
controller.namespaces.Add(tc.AddedNamespace)
controller.namespaceAdded(tc.AddedNamespace)
}
if tc.UpdatedNamespace != nil {
controller.namespaces.Add(tc.UpdatedNamespace)
controller.namespaceUpdated(nil, tc.UpdatedNamespace)
}
if tc.DeletedServiceAccount != nil {
controller.serviceAccountDeleted(tc.DeletedServiceAccount)
}
if tc.ExpectCreatedServiceAccount {
if !handler.ValidateRequestCount(t, 1) {
t.Errorf("%s: Expected a single creation call", k)
}
} else {
if !handler.ValidateRequestCount(t, 0) {
t.Errorf("%s: Expected no creation calls", k)
}
}
testServer.Close()
}
}
...@@ -73,13 +73,16 @@ func (f *FakeHandler) ServeHTTP(response http.ResponseWriter, request *http.Requ ...@@ -73,13 +73,16 @@ func (f *FakeHandler) ServeHTTP(response http.ResponseWriter, request *http.Requ
f.RequestBody = string(bodyReceived) f.RequestBody = string(bodyReceived)
} }
func (f *FakeHandler) ValidateRequestCount(t TestInterface, count int) { func (f *FakeHandler) ValidateRequestCount(t TestInterface, count int) bool {
ok := true
f.lock.Lock() f.lock.Lock()
defer f.lock.Unlock() defer f.lock.Unlock()
if f.requestCount != count { if f.requestCount != count {
ok = false
t.Logf("Expected %d call, but got %d. Only the last call is recorded and checked.", count, f.requestCount) t.Logf("Expected %d call, but got %d. Only the last call is recorded and checked.", count, f.requestCount)
} }
f.hasBeenChecked = true f.hasBeenChecked = true
return ok
} }
// ValidateRequest verifies that FakeHandler received a request with expected path, method, and body. // ValidateRequest verifies that FakeHandler received a request with expected path, method, and body.
......
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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.
*/
// serviceaccount enforces all pods having an associated serviceaccount,
// and all containers mounting the API token for that serviceaccount at a known location
package serviceaccount
/*
Copyright 2014 The Kubernetes Authors All rights reserved.
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 e2e
import (
"fmt"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/wait"
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/serviceaccount"
. "github.com/onsi/ginkgo"
)
var _ = Describe("ServiceAccounts", func() {
var c *client.Client
var ns string
BeforeEach(func() {
var err error
c, err = loadClient()
expectNoError(err)
ns_, err := createTestingNS("service-accounts", c)
ns = ns_.Name
expectNoError(err)
})
AfterEach(func() {
// Clean up the namespace if a non-default one was used
if ns != api.NamespaceDefault {
By("Cleaning up the namespace")
err := c.Namespaces().Delete(ns)
expectNoError(err)
}
})
It("should mount an API token into pods", func() {
var tokenName string
var tokenContent string
// Standard get, update retry loop
expectNoError(wait.Poll(time.Millisecond*500, time.Second*10, func() (bool, error) {
By("getting the auto-created API token")
tokenSelector := fields.SelectorFromSet(map[string]string{client.SecretType: string(api.SecretTypeServiceAccountToken)})
secrets, err := c.Secrets(ns).List(labels.Everything(), tokenSelector)
if err != nil {
return false, err
}
if len(secrets.Items) == 0 {
return false, nil
}
if len(secrets.Items) > 1 {
return false, fmt.Errorf("Expected 1 token secret, got %d", len(secrets.Items))
}
tokenName = secrets.Items[0].Name
tokenContent = string(secrets.Items[0].Data[api.ServiceAccountTokenKey])
return true, nil
}))
pod := &api.Pod{
ObjectMeta: api.ObjectMeta{
Name: "pod-service-account-" + string(util.NewUUID()),
},
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: "service-account-test",
Image: "kubernetes/mounttest:0.1",
Args: []string{
fmt.Sprintf("--file_content=%s/%s", serviceaccount.DefaultAPITokenMountPath, api.ServiceAccountTokenKey),
},
},
},
RestartPolicy: api.RestartPolicyNever,
},
}
testContainerOutputInNamespace("consume service account token", c, pod, []string{
fmt.Sprintf(`content of file "%s/%s": %s`, serviceaccount.DefaultAPITokenMountPath, api.ServiceAccountTokenKey, tokenContent),
}, ns)
})
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment