2019-01-18 00:07:27 +00:00
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
2019-02-12 04:27:41 +00:00
"encoding/json"
2019-01-18 00:07:27 +00:00
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"time"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/smallstep/certificates/ca"
"github.com/smallstep/cli/crypto/pemutil"
"k8s.io/api/admission/v1beta1"
corev1 "k8s.io/api/core/v1"
2019-02-12 04:27:41 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2019-01-18 00:07:27 +00:00
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
var (
runtimeScheme = runtime . NewScheme ( )
codecs = serializer . NewCodecFactory ( runtimeScheme )
deserializer = codecs . UniversalDeserializer ( )
// GetRootCAPath() is broken; points to secrets not certs. So
// we'll hard code instead for now.
2019-02-12 04:27:41 +00:00
//rootCAPath = pki.GetRootCAPath()
rootCAPath = "/home/step/.step/certs/root_ca.crt"
2019-01-18 00:07:27 +00:00
)
const (
admissionWebhookAnnotationKey = "autocert.step.sm/name"
admissionWebhookStatusKey = "autocert.step.sm/status"
provisionerPasswordFile = "/home/step/password/password"
volumeMountPath = "/var/run/autocert.step.sm"
tokenSecretKey = "token"
tokenSecretLabel = "autocert.step.sm/token"
)
// Config options for the autocert admission controller.
type Config struct {
2019-02-12 04:27:41 +00:00
LogFormat string ` yaml:"logFormat" `
CaURL string ` yaml:"caUrl" `
CertLifetime string ` yaml:"certLifetime" `
Bootstrapper corev1 . Container ` yaml:"bootstrapper" `
Renewer corev1 . Container ` yaml:"renewer" `
CertsVolume corev1 . Volume ` yaml:"certsVolume" `
2019-01-18 00:07:27 +00:00
}
2019-02-12 04:27:41 +00:00
// PatchOperation represents a RFC6902 JSONPatch Operation
2019-01-18 00:07:27 +00:00
type PatchOperation struct {
2019-02-12 04:27:41 +00:00
Op string ` json:"op" `
Path string ` json:"path" `
2019-01-18 00:07:27 +00:00
Value interface { } ` json:"value,omitempty" `
}
// RFC6901 JSONPath Escaping -- https://tools.ietf.org/html/rfc6901
2019-02-12 04:27:41 +00:00
func escapeJSONPath ( path string ) string {
2019-01-18 00:07:27 +00:00
// Replace`~` with `~0` then `/` with `~1`. Note that the order
// matters otherwise we'll turn a `/` into a `~/`.
path = strings . Replace ( path , "~" , "~0" , - 1 )
path = strings . Replace ( path , "/" , "~1" , - 1 )
return path
}
func loadConfig ( file string ) ( * Config , error ) {
data , err := ioutil . ReadFile ( file )
if err != nil {
return nil , err
}
var cfg Config
if err := yaml . Unmarshal ( data , & cfg ) ; err != nil {
return nil , err
}
2019-02-12 04:27:41 +00:00
2019-01-18 00:07:27 +00:00
return & cfg , nil
}
// createTokenSecret generates a kubernetes Secret object containing a bootstrap token
// in the specified namespce. The secret name is randomly generated with a given prefix.
// A goroutine is scheduled to cleanup the secret after the token expires. The secret
// is also labelled for easy identification and manual cleanup.
func createTokenSecret ( prefix , namespace , token string ) ( string , error ) {
2019-02-12 04:27:41 +00:00
secret := corev1 . Secret {
TypeMeta : metav1 . TypeMeta {
2019-01-18 00:07:27 +00:00
Kind : "Secret" ,
APIVersion : "v1" ,
} ,
2019-02-12 04:27:41 +00:00
ObjectMeta : metav1 . ObjectMeta {
2019-01-18 00:07:27 +00:00
GenerateName : prefix ,
Namespace : namespace ,
2019-02-12 04:27:41 +00:00
Labels : map [ string ] string {
2019-01-18 00:07:27 +00:00
tokenSecretLabel : "true" ,
} ,
} ,
2019-02-12 04:27:41 +00:00
StringData : map [ string ] string {
2019-01-18 00:07:27 +00:00
tokenSecretKey : token ,
} ,
Type : corev1 . SecretTypeOpaque ,
}
client , err := NewInClusterK8sClient ( )
if err != nil {
return "" , err
}
body , err := json . Marshal ( secret )
if err != nil {
return "" , err
}
log . WithField ( "secret" , string ( body ) ) . Debug ( "Creating secret" )
req , err := client . PostRequest ( fmt . Sprintf ( "api/v1/namespaces/%s/secrets" , namespace ) , string ( body ) , "application/json" )
if err != nil {
return "" , err
}
resp , err := client . Do ( req )
if err != nil {
log . Errorf ( "Secret creation error. Response: %v" , resp )
return "" , errors . Wrap ( err , "secret creation" )
}
if resp . StatusCode < 200 || resp . StatusCode >= 300 {
log . Errorf ( "Secret creation error (!2XX). Response: %v" , resp )
var rbody [ ] byte
if resp . Body != nil {
if data , err := ioutil . ReadAll ( resp . Body ) ; err == nil {
rbody = data
}
}
log . Error ( "Error body: " , string ( rbody ) )
return "" , errors . New ( "Not 200" )
}
var rbody [ ] byte
if resp . Body != nil {
if data , err := ioutil . ReadAll ( resp . Body ) ; err == nil {
rbody = data
}
}
if len ( rbody ) == 0 {
return "" , errors . New ( "Empty response body" )
}
var created * corev1 . Secret
if err := json . Unmarshal ( rbody , & created ) ; err != nil {
return "" , errors . Wrap ( err , "Error unmarshalling secret response" )
}
// Clean up after ourselves by deleting the Secret after the bootstrap
// token expires. This is best effort -- obviously we'll miss some stuff
// if this process goes away -- but the secrets are also labelled so
// it's also easy to clean them up in bulk using kubectl if we miss any.
go func ( ) {
time . Sleep ( tokenLifetime )
req , err := client . DeleteRequest ( fmt . Sprintf ( "api/v1/namespaces/%s/secrets/%s" , namespace , created . Name ) )
ctxLog := log . WithFields ( log . Fields {
2019-02-12 04:27:41 +00:00
"name" : created . Name ,
2019-01-18 00:07:27 +00:00
"namespace" : namespace ,
} )
if err != nil {
ctxLog . WithField ( "error" , err ) . Error ( "Error deleting expired boostrap token secret" )
return
}
resp , err := client . Do ( req )
if err != nil {
ctxLog . WithField ( "error" , err ) . Error ( "Error deleting expired boostrap token secret" )
return
}
if resp . StatusCode < 200 || resp . StatusCode >= 300 {
ctxLog . WithFields ( log . Fields {
2019-02-12 04:27:41 +00:00
"status" : resp . Status ,
2019-01-18 00:07:27 +00:00
"statusCode" : resp . StatusCode ,
} ) . Error ( "Error deleting expired boostrap token secret" )
return
}
ctxLog . Info ( "Deleted expired bootstrap token secret" )
} ( )
return created . Name , err
}
// mkBootstrapper generates a bootstrap container based on the template defined in Config. It
// generates a new bootstrap token and mounts it, along with other required coniguration, as
// environment variables in the returned bootstrap container.
func mkBootstrapper ( config * Config , commonName string , namespace string , provisioner Provisioner ) ( corev1 . Container , error ) {
b := config . Bootstrapper
token , err := provisioner . Token ( commonName )
if err != nil {
return b , errors . Wrap ( err , "token generation" )
}
// Generate CA fingerprint
crt , err := pemutil . ReadCertificate ( rootCAPath )
if err != nil {
return b , errors . Wrap ( err , "CA fingerprint" )
}
sum := sha256 . Sum256 ( crt . Raw )
fingerprint := strings . ToLower ( hex . EncodeToString ( sum [ : ] ) )
2019-02-12 04:27:41 +00:00
secretName , err := createTokenSecret ( commonName + "-" , namespace , token )
2019-01-18 00:07:27 +00:00
if err != nil {
return b , errors . Wrap ( err , "create token secret" )
}
log . Infof ( "Secret name is: %s" , secretName )
2019-02-12 04:27:41 +00:00
b . Env = append ( b . Env , corev1 . EnvVar {
Name : "COMMON_NAME" ,
2019-01-18 00:07:27 +00:00
Value : commonName ,
} )
2019-02-12 04:27:41 +00:00
b . Env = append ( b . Env , corev1 . EnvVar {
2019-01-18 00:07:27 +00:00
Name : "STEP_TOKEN" ,
2019-02-12 04:27:41 +00:00
ValueFrom : & corev1 . EnvVarSource {
SecretKeyRef : & corev1 . SecretKeySelector {
LocalObjectReference : corev1 . LocalObjectReference {
2019-01-18 00:07:27 +00:00
Name : secretName ,
} ,
Key : tokenSecretKey ,
} ,
} ,
} )
2019-02-12 04:27:41 +00:00
b . Env = append ( b . Env , corev1 . EnvVar {
Name : "STEP_CA_URL" ,
Value : config . CaURL ,
2019-01-18 00:07:27 +00:00
} )
2019-02-12 04:27:41 +00:00
b . Env = append ( b . Env , corev1 . EnvVar {
Name : "STEP_FINGERPRINT" ,
2019-01-18 00:07:27 +00:00
Value : fingerprint ,
} )
2019-02-12 04:27:41 +00:00
b . Env = append ( b . Env , corev1 . EnvVar {
Name : "STEP_NOT_AFTER" ,
2019-01-18 00:07:27 +00:00
Value : config . CertLifetime ,
} )
return b , nil
}
// mkRenewer generates a new renewer based on the template provided in Config.
2019-02-12 04:27:41 +00:00
func mkRenewer ( config * Config ) corev1 . Container {
2019-01-18 00:07:27 +00:00
r := config . Renewer
2019-02-12 04:27:41 +00:00
r . Env = append ( r . Env , corev1 . EnvVar {
Name : "STEP_CA_URL" ,
Value : config . CaURL ,
2019-01-18 00:07:27 +00:00
} )
return r
}
func addContainers ( existing , new [ ] corev1 . Container , path string ) ( ops [ ] PatchOperation ) {
if len ( existing ) == 0 {
2019-02-12 04:27:41 +00:00
return [ ] PatchOperation {
{
Op : "add" ,
Path : path ,
2019-01-18 00:07:27 +00:00
Value : new ,
} ,
}
}
2019-02-12 04:27:41 +00:00
for _ , add := range new {
ops = append ( ops , PatchOperation {
Op : "add" ,
Path : path + "/-" ,
Value : add ,
} )
}
return ops
2019-01-18 00:07:27 +00:00
}
func addVolumes ( existing , new [ ] corev1 . Volume , path string ) ( ops [ ] PatchOperation ) {
if len ( existing ) == 0 {
return [ ] PatchOperation {
2019-02-12 04:27:41 +00:00
{
2019-01-18 00:07:27 +00:00
Op : "add" ,
Path : path ,
Value : new ,
} ,
}
}
2019-02-12 04:27:41 +00:00
for _ , add := range new {
ops = append ( ops , PatchOperation {
Op : "add" ,
Path : path + "/-" ,
Value : add ,
} )
}
return ops
2019-01-18 00:07:27 +00:00
}
func addCertsVolumeMount ( volumeName string , containers [ ] corev1 . Container ) ( ops [ ] PatchOperation ) {
2019-02-12 04:27:41 +00:00
volumeMount := corev1 . VolumeMount {
Name : volumeName ,
2019-01-18 00:07:27 +00:00
MountPath : volumeMountPath ,
2019-02-12 04:27:41 +00:00
ReadOnly : true ,
2019-01-18 00:07:27 +00:00
}
for i , container := range containers {
if len ( container . VolumeMounts ) == 0 {
2019-02-12 04:27:41 +00:00
ops = append ( ops , PatchOperation {
2019-01-18 00:07:27 +00:00
Op : "add" ,
Path : fmt . Sprintf ( "/spec/containers/%v/volumeMounts" , i ) ,
Value : [ ] corev1 . VolumeMount { volumeMount } ,
} )
} else {
2019-02-12 04:27:41 +00:00
ops = append ( ops , PatchOperation {
2019-01-18 00:07:27 +00:00
Op : "add" ,
Path : fmt . Sprintf ( "/spec/containers/%v/volumeMounts/-" , i ) ,
Value : volumeMount ,
} )
}
}
return ops
}
func addAnnotations ( existing , new map [ string ] string ) ( ops [ ] PatchOperation ) {
if len ( existing ) == 0 {
return [ ] PatchOperation {
2019-02-12 04:27:41 +00:00
{
2019-01-18 00:07:27 +00:00
Op : "add" ,
Path : "/metadata/annotations" ,
Value : new ,
} ,
}
}
for k , v := range new {
if existing [ k ] == "" {
2019-02-12 04:27:41 +00:00
ops = append ( ops , PatchOperation {
2019-01-18 00:07:27 +00:00
Op : "add" ,
2019-02-12 04:27:41 +00:00
Path : "/metadata/annotations/" + escapeJSONPath ( k ) ,
2019-01-18 00:07:27 +00:00
Value : v ,
} )
} else {
2019-02-12 04:27:41 +00:00
ops = append ( ops , PatchOperation {
Op : "replace" ,
Path : "/metadata/annotations/" + escapeJSONPath ( k ) ,
2019-01-18 00:07:27 +00:00
Value : v ,
} )
}
}
return ops
}
// patch produces a list of patches to apply to a pod to inject a certificate. In particular,
// we patch the pod in order to:
// - Mount the `certs` volume in existing containers defined in the pod
// - Add the autocert-renewer as a container (a sidecar)
// - Add the autocert-bootstrapper as an initContainer
// - Add the `certs` volume definition
// - Annotate the pod to indicate that it's been processed by this controller
// The result is a list of serialized JSONPatch objects (or an error).
func patch ( pod * corev1 . Pod , namespace string , config * Config , provisioner Provisioner ) ( [ ] byte , error ) {
2019-02-12 04:27:41 +00:00
var ops [ ] PatchOperation
2019-01-18 00:07:27 +00:00
commonName := pod . ObjectMeta . GetAnnotations ( ) [ admissionWebhookAnnotationKey ]
renewer := mkRenewer ( config )
bootstrapper , err := mkBootstrapper ( config , commonName , namespace , provisioner )
if err != nil {
return nil , err
}
ops = append ( ops , addCertsVolumeMount ( config . CertsVolume . Name , pod . Spec . Containers ) ... )
ops = append ( ops , addContainers ( pod . Spec . Containers , [ ] corev1 . Container { renewer } , "/spec/containers" ) ... )
ops = append ( ops , addContainers ( pod . Spec . InitContainers , [ ] corev1 . Container { bootstrapper } , "/spec/initContainers" ) ... )
ops = append ( ops , addVolumes ( pod . Spec . Volumes , [ ] corev1 . Volume { config . CertsVolume } , "/spec/volumes" ) ... )
ops = append ( ops , addAnnotations ( pod . Annotations , map [ string ] string { admissionWebhookStatusKey : "injected" } ) ... )
return json . Marshal ( ops )
}
// shouldMutate checks whether a pod is subject to mutation by this admission controller. A pod
// is subject to mutation if it's annotated with the `admissionWebhookAnnotationKey` and if it
// has not already been processed (indicated by `admissionWebhookStatusKey` set to `injected`).
func shouldMutate ( metadata * metav1 . ObjectMeta ) bool {
annotations := metadata . GetAnnotations ( )
if annotations == nil {
annotations = map [ string ] string { }
}
// Only mutate if the object is annotated appropriately (annotation key set) and we haven't
// mutated already (status key isn't set).
if annotations [ admissionWebhookAnnotationKey ] == "" || annotations [ admissionWebhookStatusKey ] == "injected" {
return false
}
2019-02-12 04:27:41 +00:00
return true
2019-01-18 00:07:27 +00:00
}
// mutate takes an `AdmissionReview`, determines whether it is subject to mutation, and returns
// an appropriate `AdmissionResponse` including patches or any errors that occurred.
func mutate ( review * v1beta1 . AdmissionReview , config * Config , provisioner Provisioner ) * v1beta1 . AdmissionResponse {
ctxLog := log . WithField ( "uid" , review . Request . UID )
request := review . Request
var pod corev1 . Pod
if err := json . Unmarshal ( request . Object . Raw , & pod ) ; err != nil {
ctxLog . WithField ( "error" , err ) . Error ( "Error unmarshalling pod" )
2019-02-12 04:27:41 +00:00
return & v1beta1 . AdmissionResponse {
2019-01-18 00:07:27 +00:00
Allowed : false ,
UID : request . UID ,
2019-02-12 04:27:41 +00:00
Result : & metav1 . Status {
2019-01-18 00:07:27 +00:00
Message : err . Error ( ) ,
} ,
}
}
ctxLog = ctxLog . WithFields ( log . Fields {
2019-02-12 04:27:41 +00:00
"kind" : request . Kind ,
"operation" : request . Operation ,
"name" : pod . Name ,
2019-01-18 00:07:27 +00:00
"generateName" : pod . GenerateName ,
2019-02-12 04:27:41 +00:00
"namespace" : request . Namespace ,
"user" : request . UserInfo ,
2019-01-18 00:07:27 +00:00
} )
if ! shouldMutate ( & pod . ObjectMeta ) {
ctxLog . WithField ( "annotations" , pod . Annotations ) . Info ( "Skipping mutation" )
2019-02-12 04:27:41 +00:00
return & v1beta1 . AdmissionResponse {
2019-01-18 00:07:27 +00:00
Allowed : true ,
UID : request . UID ,
}
}
patchBytes , err := patch ( & pod , request . Namespace , config , provisioner )
if err != nil {
ctxLog . WithField ( "error" , err ) . Error ( "Error generating patch" )
2019-02-12 04:27:41 +00:00
return & v1beta1 . AdmissionResponse {
2019-01-18 00:07:27 +00:00
Allowed : false ,
UID : request . UID ,
2019-02-12 04:27:41 +00:00
Result : & metav1 . Status {
2019-01-18 00:07:27 +00:00
Message : err . Error ( ) ,
} ,
}
}
ctxLog . WithField ( "patch" , string ( patchBytes ) ) . Info ( "Generated patch" )
2019-02-12 04:27:41 +00:00
return & v1beta1 . AdmissionResponse {
Allowed : true ,
Patch : patchBytes ,
UID : request . UID ,
2019-01-18 00:07:27 +00:00
PatchType : func ( ) * v1beta1 . PatchType {
pt := v1beta1 . PatchTypeJSONPatch
return & pt
} ( ) ,
}
}
func main ( ) {
if len ( os . Args ) != 2 {
log . Errorf ( "Usage: %s <config>\n" , os . Args [ 0 ] )
os . Exit ( 1 )
}
config , err := loadConfig ( os . Args [ 1 ] )
if err != nil {
panic ( err )
}
log . SetOutput ( os . Stdout )
if config . LogFormat == "json" {
log . SetFormatter ( & log . JSONFormatter { } )
}
if config . LogFormat == "text" {
log . SetFormatter ( & log . TextFormatter { } )
}
log . WithFields ( log . Fields {
"config" : config ,
} ) . Info ( "Loaded config" )
provisionerName := os . Getenv ( "PROVISIONER_NAME" )
provisionerKid := os . Getenv ( "PROVISIONER_KID" )
log . WithFields ( log . Fields {
"provisionerName" : provisionerName ,
2019-02-12 04:27:41 +00:00
"provisionerKid" : provisionerKid ,
2019-01-18 00:07:27 +00:00
} ) . Info ( "Loaded provisioner configuration" )
2019-02-12 04:27:41 +00:00
provisioner , err := NewProvisioner ( provisionerName , provisionerKid , config . CaURL , rootCAPath , provisionerPasswordFile )
2019-01-18 00:07:27 +00:00
if err != nil {
log . Errorf ( "Error loading provisioner: %v" , err )
os . Exit ( 1 )
}
log . WithFields ( log . Fields {
"name" : provisioner . Name ( ) ,
2019-02-12 04:27:41 +00:00
"kid" : provisioner . Kid ( ) ,
2019-01-18 00:07:27 +00:00
} ) . Info ( "Loaded provisioner" )
namespace := os . Getenv ( "NAMESPACE" )
if namespace == "" {
log . Errorf ( "$NAMESPACE not set" )
os . Exit ( 1 )
}
name := fmt . Sprintf ( "autocert.%s.svc" , namespace )
token , err := provisioner . Token ( name )
if err != nil {
log . WithField ( "error" , err ) . Errorf ( "Error generating bootstrap token during controller startup" )
os . Exit ( 1 )
}
log . WithField ( "name" , name ) . Infof ( "Generated bootstrap token for controller" )
// make sure to cancel the renew goroutine
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
srv , err := ca . BootstrapServer ( ctx , token , & http . Server {
Addr : ":4443" ,
Handler : http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
if r . URL . Path == "/healthz" {
log . Info ( "/healthz" )
fmt . Fprintf ( w , "ok" )
w . WriteHeader ( http . StatusOK )
return
}
/ *
2019-02-12 04:27:41 +00:00
var name string
if r . TLS != nil && len ( r . TLS . PeerCertificates ) > 0 {
name = r . TLS . PeerCertificates [ 0 ] . Subject . CommonName
}
2019-01-18 00:07:27 +00:00
* /
if r . URL . Path != "/mutate" {
log . WithField ( "path" , r . URL . Path ) . Error ( "Bad Request: 404 Not Found" )
http . NotFound ( w , r )
return
}
var body [ ] byte
if r . Body != nil {
if data , err := ioutil . ReadAll ( r . Body ) ; err == nil {
body = data
}
}
if len ( body ) == 0 {
log . Error ( "Bad Request: 400 (Empty Body)" )
http . Error ( w , "Bad Request (Empty Body)" , http . StatusBadRequest )
return
}
contentType := r . Header . Get ( "Content-Type" )
if contentType != "application/json" {
log . WithField ( "Content-Type" , contentType ) . Error ( "Bad Request: 415 (Unsupported Media Type)" )
http . Error ( w , fmt . Sprintf ( "Bad Request: 415 Unsupported Media Type (Expected Content-Type 'application/json' but got '%s')" , contentType ) , http . StatusUnsupportedMediaType )
return
}
var response * v1beta1 . AdmissionResponse
review := v1beta1 . AdmissionReview { }
if _ , _ , err := deserializer . Decode ( body , nil , & review ) ; err != nil {
log . WithFields ( log . Fields {
2019-02-12 04:27:41 +00:00
"body" : body ,
2019-01-18 00:07:27 +00:00
"error" : err ,
} ) . Error ( "Can't decode body" )
2019-02-12 04:27:41 +00:00
response = & v1beta1 . AdmissionResponse {
2019-01-18 00:07:27 +00:00
Allowed : false ,
2019-02-12 04:27:41 +00:00
Result : & metav1 . Status {
2019-01-18 00:07:27 +00:00
Message : err . Error ( ) ,
} ,
}
} else {
response = mutate ( & review , config , provisioner )
}
2019-02-12 04:27:41 +00:00
resp , err := json . Marshal ( v1beta1 . AdmissionReview {
2019-01-18 00:07:27 +00:00
Response : response ,
} )
if err != nil {
log . WithFields ( log . Fields {
2019-02-12 04:27:41 +00:00
"uid" : review . Request . UID ,
2019-01-18 00:07:27 +00:00
"error" : err ,
} ) . Info ( "Marshal error" )
http . Error ( w , fmt . Sprintf ( "Marshal Error: %v" , err ) , http . StatusInternalServerError )
} else {
log . WithFields ( log . Fields {
2019-02-12 04:27:41 +00:00
"uid" : review . Request . UID ,
2019-01-18 00:07:27 +00:00
"response" : string ( resp ) ,
} ) . Info ( "Returning review" )
if _ , err := w . Write ( resp ) ; err != nil {
log . WithFields ( log . Fields {
2019-02-12 04:27:41 +00:00
"uid" : review . Request . UID ,
2019-01-18 00:07:27 +00:00
"error" : err ,
} ) . Info ( "Write error" )
}
}
} ) ,
} , ca . VerifyClientCertIfGiven ( ) )
if err != nil {
panic ( err )
}
log . Info ( "Listening on :4443 ..." )
if err := srv . ListenAndServeTLS ( "" , "" ) ; err != nil {
panic ( err )
}
}