自动镜像构建和加载镜像

项目地址:
buildimage: https://github.com/zhangchi6414/buildimage
buildrun: https://github.com/zhangchi6414/buildrun
s2i-operator: https://github.com/kubesphere/s2ioperator
s2irun: https://github.com/kubesphere/s2irun

根据需求,实现一个通过上传的dockerfile构建镜像,解压缩save、export压缩的镜像,以及从外部加载镜像到本地仓库功能的插件。
参照了开源工具s2i-operator实现,增加了解压缩的功能。

buildimage

1.kubebuilder生成代码

https://xuejipeng.github.io/kubebuilder-doc-cn/

kubebuilder init --domain buildimage //创建项目
kubebuilder create api --group buildimage --version v1 --kind Builder  //创建API

生成了基础的代码框架,然后开始自定义字段开发和逻辑开发

2.自定义字段开发

//自定义minio字段
type MinioOption struct {
	Endpoint   string `json:"endpoint,omitempty" `
	DisableSSL bool   `json:"disableSSL,omitempty"`
	//ForcePathStyle  string `json:"forcePathStyle,omitempty" `
	AccessKeyID     string `json:"accessKeyID,omitempty" `
	SecretAccessKey string `json:"secretAccessKey,omitempty" `
	SessionToken    string `json:"sessionToken,omitempty" `
	Bucket          string `json:"bucket,omitempty" `
	CodeName        string `json:"codeName,omitempty"`
	CodePath        string `json:"codePath,omitempty"`
}
//自定义harbor字段
type HarborOption struct {
	Endpoint   string `json:"endpoint,omitempty"`
	DisableSSL bool   `json:"disableSSL,omitempty" `
	Username   string `json:"username,omitempty" `
	Password   string `json:"password,omitempty" `
}
//自定义git字段
type GitOption struct {
	Endpoint   string `json:"endpoint,omitempty"`
	DisableSSL bool   `json:"disableSSL,omitempty" `
	Username   string `json:"username,omitempty" `
	Password   string `json:"password,omitempty" `
}
//自定义构建操作相关字段,对应crd文件中的字段
type Buildconfig struct {
	IsMinio        bool          `json:"IsMinio,omitempty"`
	Minio          *MinioOption  `json:"minio,omitempty"`
	IsSave         bool          `json:"IsSave,omitempty"`
	IsExport       bool          `json:"IsExport,omitempty"`
	HarborUrl      string        `json:"harborUrl,omitempty"`
	Harbor         *HarborOption `json:"harbor,omitempty"`
	NewImageName   string        `json:"newImageName,omitempty"`
	NewTag         string        `json:"newTag,omitempty"`
	IsGit          bool          `json:"isGit,omitempty"`
	Git            *GitOption    `json:"git,omitempty"`
	DockerfileName string        `json:"dockerfileName,omitempty"`
	BackLimit      int32         `json:"backLimit,omitempty"`
	SaveImageName  string        `json:"saveImageName,omitempty"`
}

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// BuilderSpec defines the desired state of Builder
type BuilderSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	Config *Buildconfig `json:"config,omitempty" `
}

//定义自定义资源状态的相关字段
// BuilderStatus defines the observed state of Builder
type BuilderStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	//RunCount represent the sum of s2irun of this builder
	RunCount int `json:"runCount"`
	//LastRunState return the state of the newest run of this builder
	LastRunState RunState `json:"lastRunState,omitempty"`
	//LastRunState return the name of the newest run of this builder
	LastRunName *string `json:"lastRunName,omitempty"`
	//LastRunStartTime return the startTime of the newest run of this builder
	LastRunStartTime *metav1.Time `json:"lastRunStartTime,omitempty"`
}

// +genclient

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// Builder is the Schema for the builders API
type Builder struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   BuilderSpec   `json:"spec,omitempty"`
	Status BuilderStatus `json:"status,omitempty"`
}

3.逻辑开发

创建自定义资源---读取字段---创建对应的job---通过buid容器来拉取代码构建镜像

读取字段

func (r *BuilderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	//_ = log.FromContext(ctx)
	log.Info("开始执行构建任务:", "Name", req.Name)
	//判断资源是否被创建
	instance := &buildimagev1.Builder{}
    //实例化对象,把创建的builder中的字段和结构体绑定
	err := r.Get(ctx, req.NamespacedName, instance)
	if err != nil {
		if errors.IsNotFound(err) {
			fmt.Println("not found resource~!")
			return ctrl.Result{}, err
		}
		return ctrl.Result{}, err
	}
	crs, err := createRBAC(ctx, r, instance)
	if err != nil {
		return crs, err
	}
	//查看job是否存在,不存在则创建
	Job := &v1.Job{}
	err = r.Get(ctx, types.NamespacedName{Namespace: instance.Namespace, Name: instance.Name}, Job)
	if err != nil {
		if errors.IsNotFound(err) {
			res, err := createJob(ctx, r, instance)
			return res, err
		} else {
			return ctrl.Result{}, err
		}
	}
	//检查job状态,更新build状态

	return ctrl.Result{}, nil
}

创建job

type Jobs struct {
	VolumeMount []v1.VolumeMount
	Volume      []v1.Volume
	Env         []v1.EnvVar
}
//创建job任务
func (j *Jobs) CreateJob(instance *buildimagev1.Builder) (*v12.Job, error) {
	var job = &v12.Job{}
	jobName := instance.Name + fmt.Sprintf("-%s", utils.Randow()+"-job")
	imageName := os.Getenv("BUILDIMAGENAME")
	//TODO 测试
	//if imageName == "" {
	//	return nil, fmt.Errorf("Failed to get s2i-image name, please set the env 'S2IIMAGENAME' ")
	//}
	//TODO 默认镜像需要替换
	if imageName == "" {
		imageName = "Alpine"
	}
	job = &v12.Job{
		ObjectMeta: metav1.ObjectMeta{
			Name:      jobName,
			Namespace: instance.ObjectMeta.Namespace,
		},
		Spec: v12.JobSpec{
			Template: v1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{"job-name": jobName},
				},
				Spec: v1.PodSpec{
					ServiceAccountName: RegularServiceAccount,
					Containers: []v1.Container{
						{
							Name:            "buildimage",
							Image:           imageName,
							Command:         []string{"./builder"},
							ImagePullPolicy: v1.PullIfNotPresent,
							Env:             j.Env,
							VolumeMounts:    j.VolumeMount,
							SecurityContext: &v1.SecurityContext{
								Privileged: truePtr(),
							},
						},
					},
					RestartPolicy: v1.RestartPolicyNever,
					Volumes:       j.Volume,
				},
			},
			BackoffLimit: &instance.Spec.Config.BackLimit,
		},
	}
	return job, nil
}

判断不同不模式挂载不同变量和文件

func slectFunc(instance *buildimagev1.Builder) (*v1.Job, error) {
	//判断获取代码文件的方式
	jobs := &pkg.Jobs{}
	if instance.Spec.Config.IsMinio {
		// TODO 创建从minio获取源码的方式
		jobs = &pkg.Jobs{
			Volume: []corev1.Volume{
				{
					Name: "dockerfile",
					VolumeSource: corev1.VolumeSource{
						ConfigMap: &corev1.ConfigMapVolumeSource{
							LocalObjectReference: corev1.LocalObjectReference{
								Name: instance.Spec.Config.DockerfileName,
							},
							Items: []corev1.KeyToPath{
								{
									Key:  pkg.ConfigDataKey,
									Path: "Dockerfile",
								},
							},
						},
					},
				},
				{
					Name: "docker-sock",
					VolumeSource: corev1.VolumeSource{
						HostPath: &corev1.HostPathVolumeSource{
							Path: "/var/run/docker.sock",
						},
					},
				},
			},
			VolumeMount: []corev1.VolumeMount{
				{
					Name:      "docker-sock",
					MountPath: "/var/run/docker.sock",
				},
				{
					Name:      "dockerfile",
					MountPath: "/config",
				},
			},
			Env: []corev1.EnvVar{
				{
					Name:  "MinioUrl",
					Value: instance.Spec.Config.Minio.Endpoint,
				},
			....
	}
	if instance.Spec.Config.IsSave {
		// TODO 创建通过Save方式上传镜像的方法
			....
	}
	if instance.Spec.Config.IsExport {
        // TODO 创建通过Export方式上传镜像的方法
			....
	}
	if instance.Spec.Config.IsGit {
		// TODO 创建从git获取源码的方式
				...

	}
	job, err := jobs.CreateJob(instance)
	if err != nil {
		return nil, err
	}
	return job, err
}

buildRun

实现代码拉取,镜像导入,镜像推送

代码拉取

func (o *MinioOption) Pull(cli *minio.Client) error {
	fmt.Println(&cli)
	zap.S().Info("Start download code!")
	ctx := context.Background()
	err := cli.FGetObject(ctx, o.Bucket, o.CodePath+o.CodeName, o.ForcePathStyle+o.CodeName, minio.GetObjectOptions{})
	if err != nil {
		zap.S().Error(err)
		return nil
	}
	zap.S().Info("Download the file successful!")
	return nil
}

镜像构建

func (d *stiDocker) BuildImage(cli *client.Client, codeName, name string) error {
	var tags = []string{name}
	fileOptions := types.ImageBuildOptions{
		Tags:           tags,
		Dockerfile:     "docker/Dockerfile",
		SuppressOutput: false,
		Remove:         true,
		ForceRemove:    true,
		PullParent:     true,
	}
	//拷贝Dockerfile
	err := utils.Copy(pkg.DOCKERFILE, pkg.DOCKERFILEPATH+"Dockerfile")
	if err != nil {
		return err
	}
	//拷贝代码文件
	err = utils.Copy(codeName, pkg.DOCKERFILEPATH+codeName)
	if err != nil {
		return err
	}
	var destTar = "docker.tar"
	//把文件打成tar包
	err = utils.Tar(pkg.DOCKERFILEPATH, destTar, false)
	if err != nil {
		zap.S().Error(err)
		return err
	}
	//执行构建
	zap.S().Info("Start build image:", name)
	ctx := context.Background()
	dockerBuildContext, err := os.Open(destTar)
	if err != nil {
		return err
	}
	defer dockerBuildContext.Close()
	buildResponse, err := cli.ImageBuild(ctx, dockerBuildContext, fileOptions)
	if err != nil {
		zap.S().Error(err)
		return err
	}
	_ = logImage(buildResponse.Body)
	zap.S().Info("Start build image:", name, "success!")
	err = d.PushImage(cli, name)
	if err != nil {
		zap.S().Error(err)
		return err
	}
	return nil
}

镜像导入

func (d *stiDocker) LoadImage(cli *client.Client, code, oldName, name string) error {
	//打开镜像文件
	imageFile, err := os.Open(code)
	if err != nil {
		zap.S().Error(err)
	}
	defer imageFile.Close()
	ctx := context.Background()
	zap.S().Info("Start load image")
	load, err := cli.ImageLoad(ctx, imageFile, true)

	defer load.Body.Close()
	//load镜像
	str := logImage(load.Body)
	//_ = logImage(load.Body)
	if err != nil {
		os.Exit(pkg.LOADIMAGEERROR)
		zap.S().Error(err)
	}
	//获取load后的镜像名称
	start := strings.Index(str, ": ") + 2
	end := strings.Index(str[start:], "\\n")
	imageName := str[start : start+end]
	zap.S().Info("Image load success!")
	if name == "" {
		name = imageName
	}
	err = cli.ImageTag(ctx, oldName, name)
	if err != nil {
		return err
	}
	//导入镜像
	err = d.PushImage(cli, name)
	if err != nil {
		return err
	}
	return nil
}

func (d *stiDocker) ImportImage(cli *client.Client, name, imageName string) error {
	//读取镜像文件
	imageFile, err := os.Open(name)
	defer imageFile.Close()
	if err != nil {
		return err
	}
	options := types.ImageImportOptions{}
	source := types.ImageImportSource{
		Source:     imageFile,
		SourceName: "-",
	}
	//import镜像文件
	ctx := context.Background()
	zap.S().Info("Start import image!")
	imageImport, err := cli.ImageImport(ctx, source, imageName, options)
	defer imageImport.Close()
	_ = logImage(imageImport)
	if err != nil {
		os.Exit(pkg.IMPORTIMAGEERROR)
		return err
	}
	//推送镜像
	err = d.PushImage(cli, imageName)

	if err != nil {
		return err
	}
	return nil
}

镜像推送

func (d *stiDocker) PushImage(cli *client.Client, name string) error {
	//harbor认证
	authConfig := types.AuthConfig{
		Username: d.UserName,
		Password: d.Password,
	}
	authStr, err := encodeAuthToBase64(authConfig)
	if err != nil {
		zap.S().Error(err)
		return err
	}
	//读取镜像文件
	zap.S().Info("start push image:", name)
	var pushReader io.ReadCloser
	pushReader, err = cli.ImagePush(context.Background(), name, types.ImagePushOptions{
		All:           false,
		RegistryAuth:  authStr,
		PrivilegeFunc: nil,
	})
	defer pushReader.Close()
	//输出推送进度
	_ = logImage(pushReader)
	if err != nil {
		os.Exit(pkg.PUSHIMAGEERROR)
		zap.S().Error(err)
		return err
	}
	zap.S().Info("push success ! ", name)
	return nil
}

使用方式

Dockerfile

#需要通过ConfigMap挂载Dockerfile
apiVersion: v1
kind: ConfigMap
metadata:
  name: game-demo
data:
  dockerfile: |    #key值固定
    FROM 192.168.2.106:1180/456/alpine:3.6
    COPY docker/address.png /root/
    CMD ["sleep 30000"]

1.自定义builder
apiVersion: buildimage.buildimage/v1  #自定义的crd资源对象
kind: Builder
metadata:
  name: build-test
spec:
  config:
    IsMinio: true   #改模式为自定义Dockerfile,不是自定义Dockerfile这个关键字可不写
    harbor:   #harbor信息
      username: admin
      password: Dyg@12345
    newImageName: 192.168.2.106:1180/4561/fzzn-test1  #新构建镜像的名称
    newTag: v3  #新镜像tag
    dockerfileName: game-demo  #指定构建镜像的Dockerfile 需要在同一namespace
    backLimit: 1   #job出错重启次数限制
    fromImage: 192.168.2.106:1180/4561/fzzn:v1  #Dockerfile中的基础镜像名称

2.上传通过save生成的rar/tar格式镜像文件
apiVersion: buildimage.buildimage/v1
kind: Builder
metadata:
  name: build-test
spec:
  config:
    IsSave: true
    saveImageName:  redis:5.0.7  #打包之前的镜像名称  记不起可以不填
    minio:       #minio的信息
      accessKeyID: admin
      secretAccessKey: abcdefg123456
      endpoint: 192.168.2.108:30900  #注意格式要一致,不能有Http/https
      codePath: /fz-1/ay/  #文件在minio中的路径要一致不能少 /
      bucket: dyg-fzzn     #minio中的bucket
      codeName: redis.rar   #需要从minio中下载的文件
    harbor:
      username: admin
      password: Dyg@12345
    newImageName: 192.168.2.106:1180/456/redis
    newTag: v3
    backLimit: 1
3.上传通过export生成的Img格式镜像文件
apiVersion: buildimage.buildimage/v1
kind: Builder
metadata:
  name: build-test
spec:
  config:
    IsExport: true
    minio:
      accessKeyID: admin
      secretAccessKey: abcdefg123456
      endpoint: 192.168.2.108:30900
      codePath: /fz-1/
      bucket: dyg-fzzn
      codeName: import.img   #文件名称不要错了
    harbor:
      username: admin
      password: Dyg@12345
    newImageName: 192.168.2.106:1180/456/img
    newTag: v3
    backLimit: 1

部署方式

make install 
make deploy
make undeploy
posted @ 2023-06-02 15:44  惊蛰2020  阅读(139)  评论(0编辑  收藏  举报