Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add volumes implementation support #88

Merged
merged 1 commit into from
May 5, 2017

Conversation

surajssd
Copy link
Contributor

@surajssd surajssd commented Apr 3, 2017

This PR adds support for volumes in OpenCompose.

With this there is new root level directive called volumes and new directive in container level called
mounts and also adds service level directive called emptyDirVolumes.

More info about volumes is in the proposal: https://github.com/surajssd/opencompose/blob/22207ea605580d70c6718fa87368d0385bc3cbf0/docs/proposals/volumes-proposal.md

Fixes #25

@surajssd
Copy link
Contributor Author

surajssd commented Apr 4, 2017

using opencompose file:

version: 0.1-dev

services:
- name: db
  containers:
  - image: mysql
    mounts:
    - volumeName: db
      mountPath: /var/lib/mysql

- name: backup
  containers:
  - image: backup
    mounts:
    - volumeName: db
      mountPath: /app/store
      volumeSubPath: foo/bar
      readOnly: true

- name: process
  containers:
  - image: process
    mounts:
    - volumeName: temp
      mountPath: /app/data
  emptyDirVolumes:
  - name: temp

volumes:
- name: db
  size: 5Gi
  accessMode: ReadWriteMany
  storageClass: fast

converting as:

$ opencompose convert -f ex.yml -o - 
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    service: db
  name: db
spec:
  strategy:
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        service: db
    spec:
      containers:
      - image: mysql
        name: db-0
        resources: {}
        volumeMounts:
        - mountPath: /var/lib/mysql
          name: db
      volumes:
      - name: db
        persistentVolumeClaim:
          claimName: db
status: {}
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    service: backup
  name: backup
spec:
  strategy:
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        service: backup
    spec:
      containers:
      - image: backup
        name: backup-0
        resources: {}
        volumeMounts:
        - mountPath: /app/store
          name: db
          readOnly: true
          subPath: foo/bar
      volumes:
      - name: db
        persistentVolumeClaim:
          claimName: db
status: {}
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    service: process
  name: process
spec:
  strategy:
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        service: process
    spec:
      containers:
      - image: process
        name: process-0
        resources: {}
        volumeMounts:
        - mountPath: /app/data
          name: temp
      volumes:
      - emptyDir: {}
        name: temp
status: {}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  annotations:
    volume.beta.kubernetes.io/storage-class: fast
  creationTimestamp: null
  name: db
spec:
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 5Gi
status: {}

@surajssd
Copy link
Contributor Author

surajssd commented Apr 4, 2017

Tests are pending but would appreciate feedback to see if I am going right?
ping @kadel @tnozicka

@surajssd
Copy link
Contributor Author

was clicked by mistake

@surajssd surajssd reopened this Apr 10, 2017
@surajssd
Copy link
Contributor Author

@kadel For testing the parsing part/encoding_test.go I am considering following test case types:

type pass or fail optional comment
All fields given
Required fields only given/no optional field
Extra field given
Wrong field types
No fields given at all ✅ if all fields are optional otherwise ❌
One required field not given

Do you feel anything is missing and I should think of something else?

@surajssd surajssd force-pushed the volumes-implementation branch 4 times, most recently from f8070a9 to a19a891 Compare April 11, 2017 10:31
}

// validate mountPath
if err := validatePath(mount.MountPath); err != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kadel should we do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or just assume that user will give valid path?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If i'm not mistaken MountPath should be always absolute path. We could check at least that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

@surajssd surajssd force-pushed the volumes-implementation branch 4 times, most recently from dfa2e8e to d559494 Compare April 11, 2017 18:05
mounts:
- volumeName: db
mountPath: /app/store
volumeSubPath: foo/bar

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Total thumbsup for this feature, I do need it for my usecase:

https://github.com/brainstorm/cgtd/commit/344fa1a15d5874c3dff17e91537864b77b5fd2b1

Would it be reasonable to include support for paths like $HOME in the spec?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be reasonable to include support for paths like $HOME in the spec?

That is interesting idea. We won't add it in this initial implementation, but definitely will keep it in mind for future improvements. Will have to do some research on how this can be done in Kubernetes. (any pointers?)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea, unfortunately, quite a newbie on kubernetes but I know from related-ish projects like:

https://github.com/BD2KGenomics/toil
https://github.com/common-workflow-language/common-workflow-language

That users are gonna expect it to be supported :-S

Copy link
Collaborator

@concaf concaf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@surajssd How about we move the type declarations to object.go?

@@ -179,10 +179,55 @@ type ImageRef string

// FIXME: implement ImageRef unmarshalling

type Path string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we move the type declaration to object.go and do something like type Path object.Path here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess in this case its ok. As there is no object.Path its inside Mount struct.

But if we don't need custom UnmarshalYAML for Path than we don't even need Path as separate type here.
We can use just string instead of Path in Mount struct

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return nil
}

type Mount struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we move the type declaration to object.go

@@ -205,10 +250,34 @@ func (c *Container) UnmarshalYAML(unmarshal func(interface{}) error) error {
return nil
}

type EmptyDirVolume struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we move the type declaration to object.go

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs json tags so keeping it here.
Ref: @tnozicka 's explanation

@@ -189,14 +190,91 @@ func (t *Transformer) CreateDeployments(o *object.Service) ([]runtime.Object, er
})
}

// TODO: It is assumed that the check is done about the existence of volume in root level volume section
for _, mount := range c.Mounts {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove this \n

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Member

@kadel kadel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is my first pass on this.

Didn't have time to look at tests yet :-(

@@ -233,16 +302,37 @@ func (s *Service) UnmarshalYAML(unmarshal func(interface{}) error) error {

type VolumeSize string

// FIXME: add VolumeSize parsing/validation
func (vs *VolumeSize) UnmarshalYAML(unmarshal func(interface{}) error) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no validation or additional parsing is done here, this function is not needed, is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it as it's done for ResourceName here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UnmarshalYAML for ResourceName does validation on top of unmarshalling .
But in this case you are not doing anything special, you are just calling unmarshal function on string type. You don't need custom UnmarshalYAML function in this case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed those types changed it to built in type string


type VolumeMode string

// FIXME: add VolumeMode parsing/validation
func (vm *VolumeMode) UnmarshalYAML(unmarshal func(interface{}) error) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no validation or additional parsing is done here, this function is not needed, is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see the comment above!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment above ;-)

@@ -179,10 +179,55 @@ type ImageRef string

// FIXME: implement ImageRef unmarshalling

type Path string

func (p *Path) UnmarshalYAML(unmarshal func(interface{}) error) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no validation or additional parsing is done here, this function is not needed, is it?

@@ -50,16 +71,129 @@ type OpenCompose struct {
Volumes []Volume
}

func (s *Service) EmptyDirVolumeExists(name string) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please add comment explaining what this function is for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return false
}

func (o *OpenCompose) VolumeExists(name string) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please add comment explaining what this function is for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


// Documentation about the valid identifiers can be found at
// https://github.com/kubernetes/community/blob/master/contributors/design-proposals/identifiers.md
func isValidName(name string) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is called isValidName but other validation function are called validate<something> ( validateVolumeMode
validatePath). Wouldn't be better to use same naming scheme for all function validation fields?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another issue is that when I put wrong name i get just this error:

ERROR: must match the regex [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* (e.g. 'example.com')

But it doesn't say where it is. :-(
I guess this issue will be same with other validations here. :-(

Copy link
Contributor Author

@surajssd surajssd Apr 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is called isValidName but other validation function are called validate<something> ( validateVolumeMode validatePath). Wouldn't be better to use same naming scheme for all function validation fields?

done

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another issue is that when I put wrong name i get just this error:
ERROR: must match the regex [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* (e.g. 'example.com')
But it doesn't say where it is. :-(
I guess this issue will be same with other validations here. :-(

done

return nil
}

func (v *Volume) Validate() error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this called outside object pkg?
I think this can be private. There is no reason for someone validating just volume outside of this package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking out loud, thought these could turn into interface or something similar?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interface? why? Don't know what you mean :-(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don;t remember what was I thinking :-D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@surajssd so what do you think about making this private?
I would make all Validate function private expect func (o *OpenCompose) Validate(). If we are thinking about this being used as library, this is only Validate function that should be exposed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done !

// or emptydirvolumes
for _, container := range service.Containers {
for _, mount := range container.Mounts {
if !o.VolumeExists(mount.VolumeName) && !service.EmptyDirVolumeExists(mount.VolumeName) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be one more check that validates that there are not multiple mounts for same path.

    mounts:
    - volumeName: bar
      mountPath: /storage
    - volumeName: foo
      mountPath: /storage

This is not valid and should be checked.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// if this mount does not exist in emptydir then this is coming from root level volumes directive
// if tomorrow we add support for ConfigMaps or Secrets mounted as volumes the check should be done
// here to see if it is not coming from configMaps or Secrets
if !o.EmptyDirVolumeExists(mount.VolumeName) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't be better to do if o.VolumeExists(mount.VolumeName) here? It might make more sense once we add secrets as volumes and It will be also a bit safer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have the object opencompose to do that call(o.VolumeExists). We have access to service type object o.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, that is quite confusing if its called o :-)
OK than, lets keep it that way, but it might bring some problem in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed it to s.

pvc.Spec.AccessModes = []api_v1.PersistentVolumeAccessMode{api_v1.ReadOnlyMany}
case "ReadWriteMany":
pvc.Spec.AccessModes = []api_v1.PersistentVolumeAccessMode{api_v1.ReadWriteMany}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add default section that will cause error?
I know that it shouldn't happen because its validated before, but you never know ;-) That one line might save a lot of debugging in future ;-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah SGTM, done.

@surajssd surajssd force-pushed the volumes-implementation branch 3 times, most recently from ef81fda to 6a7c60a Compare April 18, 2017 19:30
- volumeName: db
mountPath: /var/lib/mysql
- volumeName: iiit
mountPath: /var/lib/mysql
Copy link
Member

@kadel kadel Apr 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is invalid!

ERROR: service "db": container#1: mount "iiit": mountPath "/var/lib/mysql": cannot have same mountPath as "db"

allMounts := make(map[string]string)
// validate Mounts
for _, mount := range c.Mounts {
// validate volumeName
Copy link
Member

@kadel kadel Apr 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we have (c *Container) Validate() for container validation, (s *Service) Validate() for service validation, (v *Volume) Validate() for volume validation. Would it make sense to have (m *Mount) Validate() for validating single mount ?

Everything expect checking for colliding paths could be in that separate Validate function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that we might not be able to do the validation of colliding mountPath ? Since the variable allMounts is defined outside the for loop and used inside the loop, one workaround would be to pass it with every call but then it makes mount's validate function inconsistent than other validate functions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mend is to keep validation of colliding mountPath where it is.

Just move name validation ( if err := validateName(mount.VolumeName);) and path validation (if !path.IsAbs(mount.MountPath) {) to separate (m *Mount) Validate(). Those to checks don't need information about other mounts they can be separate.

Copy link
Member

@kadel kadel May 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@surajssd what do you think about my suggestion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having it in the function makes more sense than having half things here and others there

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least all those are checks in same place to avoid confusion

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still feel like this is more confusing. With other parts of OpenCompose object (service, volumes, ...) we have nicely separated validation function but here we are validating whole Mount object in Container object validation.

But if you think this is better than let's keep it like this. I can make peace with it. 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's confusing to have some tests outside in a different place and only one in here, that is why i was planning to keep it here. If the container function grows and become very huge then we can think of separating it? WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you think this is better, lets keep it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so is this good to go in?

@surajssd surajssd force-pushed the volumes-implementation branch 3 times, most recently from 36e694e to 2a620eb Compare April 25, 2017 11:02
@kadel
Copy link
Member

kadel commented Apr 25, 2017

do we need examples/vols/ex.yml? To be honest I don't like that file, as it is nonsense example that does nothing and its invalid :-(

@surajssd surajssd force-pushed the volumes-implementation branch 2 times, most recently from d0bf775 to 80f4f1c Compare April 25, 2017 13:44
@surajssd
Copy link
Contributor Author

do we need examples/vols/ex.yml? To be honest I don't like that file, as it is nonsense example that does nothing and its invalid :-(

@kadel done

This PR adds support for volumes in OpenCompose.

With this there is new root level directive called
`volumes` and new directive in container level called
`mounts` and also adds service level directive called
`emptyDirVolumes`.

More info about volumes is in the proposal:
https://github.com/surajssd/opencompose/blob/22207ea605580d70c6718fa87368d0385bc3cbf0/docs/proposals/volumes-proposal.md

Fixes redhat-developer#25
Copy link
Member

@kadel kadel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one small issue with this. Its described in #114 We don't have to block this PR because of that.

@surajssd
Copy link
Contributor Author

surajssd commented May 5, 2017

Cool will merge this then

@surajssd surajssd merged commit 5b5dc72 into redhat-developer:master May 5, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants