tweag / nickel

Better configuration for less
https://nickel-lang.org/
MIT License
2.23k stars 85 forks source link

Strange error in std.array.any #1892

Closed zefir01 closed 2 months ago

zefir01 commented 2 months ago

Hello. When importing yaml I see a strange error in std.array.any is this a bug or a feature? what am I doing wrong?

To Reproduce crossform.ncl:

let cmd=import "command.yaml" in
let Resource =
  {
    _crossform={
      id | String,
      ttt | String="ggg",
    },
    ..
  } in

let getResource = fun _id => if std.record.has_field _id cmd.observed then cmd.observed."%{_id}".resource.unstructured.object else {} in
let getConditions = fun _id => getResource _id |> match { {status, ..}=> status, _=>{conditions=[]} } |> match { {conditions, ..}=>conditions} in

let _resource : String -> Resource=fun _id => {
        _crossform={
          id:String = _id,
          ready: Bool = std.array.any (fun x => x.type=="Ready" && x.status=="True") getConditions _id && std.array.any (fun x => x.type=="Synced" && x.status=="True") getConditions _id,
          #test=getConditions _id
        }
      } & getResource _id | Resource in
{
  resource=_resource,
}

main.ncl:

let crossform = import "crossform.ncl" in
let r=crossform.resource "test1" in
r

command.yaml:

repositoryurl: git@github.com:zefir01/test2.git
repositoryrevision: main
path: project1
observed:
    test1:
        resource:
            unstructured:
                object:
                    apiVersion: kubernetes.crossplane.io/v1alpha2
                    kind: Object
                    metadata:
                        annotations:
                            crossplane.io/composition-resource-name: test1
                            crossplane.io/external-create-pending: "2024-04-16T23:49:48Z"
                            crossplane.io/external-create-succeeded: "2024-04-16T23:49:48Z"
                            crossplane.io/external-name: sample-namespace
                        creationTimestamp: "2024-04-16T23:49:48Z"
                        finalizers:
                            - finalizer.managedresource.crossplane.io
                        generateName: test1-
                        generation: 2
                        labels:
                            crossplane.io/claim-name: ""
                            crossplane.io/claim-namespace: ""
                            crossplane.io/composite: test1
                        managedFields:
                            - apiVersion: kubernetes.crossplane.io/v1alpha2
                              fieldsType: FieldsV1
                              fieldsV1:
                                f:metadata:
                                    f:annotations:
                                        f:crossplane.io/composition-resource-name: {}
                                    f:finalizers:
                                        v:"finalizer.managedresource.crossplane.io": {}
                                    f:generateName: {}
                                    f:labels:
                                        f:crossplane.io/claim-name: {}
                                        f:crossplane.io/claim-namespace: {}
                                        f:crossplane.io/composite: {}
                                    f:ownerReferences:
                                        k:{"uid":"5dc9b48b-c64d-42b5-bd98-f0515f895aed"}: {}
                                f:spec:
                                    f:deletionPolicy: {}
                                    f:forProvider:
                                        f:manifest:
                                            f:apiVersion: {}
                                            f:kind: {}
                                            f:metadata:
                                                f:labels:
                                                    f:example: {}
                                    f:managementPolicies: {}
                                    f:providerConfigRef:
                                        f:name: {}
                                    f:readiness:
                                        f:policy: {}
                              manager: apiextensions.crossplane.io/composed/e395cc1c249f744c11a49c9cf2815759b904e68321759d7e06b901eed1a97668
                              operation: Apply
                              time: "2024-04-16T23:49:48Z"
                            - apiVersion: kubernetes.crossplane.io/v1alpha2
                              fieldsType: FieldsV1
                              fieldsV1:
                                f:metadata:
                                    f:annotations:
                                        f:crossplane.io/external-create-pending: {}
                                        f:crossplane.io/external-create-succeeded: {}
                                        f:crossplane.io/external-name: {}
                                    f:finalizers:
                                        .: {}
                                        v:"finalizer.managedresource.crossplane.io": {}
                                f:spec:
                                    f:readiness:
                                        .: {}
                                        f:policy: {}
                              manager: crossplane-kubernetes-provider
                              operation: Update
                              time: "2024-04-16T23:49:48Z"
                            - apiVersion: kubernetes.crossplane.io/v1alpha2
                              fieldsType: FieldsV1
                              fieldsV1:
                                f:status:
                                    .: {}
                                    f:atProvider:
                                        .: {}
                                        f:manifest:
                                            .: {}
                                            f:apiVersion: {}
                                            f:kind: {}
                                            f:metadata:
                                                .: {}
                                                f:annotations:
                                                    .: {}
                                                    f:kubectl.kubernetes.io/last-applied-configuration: {}
                                                f:creationTimestamp: {}
                                                f:labels:
                                                    .: {}
                                                    f:example: {}
                                                    f:kubernetes.io/metadata.name: {}
                                                f:managedFields: {}
                                                f:name: {}
                                                f:resourceVersion: {}
                                                f:uid: {}
                                            f:spec:
                                                .: {}
                                                f:finalizers: {}
                                            f:status:
                                                .: {}
                                                f:phase: {}
                                    f:conditions:
                                        .: {}
                                        k:{"type":"Ready"}:
                                            .: {}
                                            f:lastTransitionTime: {}
                                            f:reason: {}
                                            f:status: {}
                                            f:type: {}
                                        k:{"type":"Synced"}:
                                            .: {}
                                            f:lastTransitionTime: {}
                                            f:reason: {}
                                            f:status: {}
                                            f:type: {}
                              manager: crossplane-kubernetes-provider
                              operation: Update
                              subresource: status
                              time: "2024-04-16T23:49:48Z"
                        name: sample-namespace
                        ownerReferences:
                            - apiVersion: crossform.io/v1alpha1
                              blockOwnerDeletion: true
                              controller: true
                              kind: xModule
                              name: test1
                              uid: 5dc9b48b-c64d-42b5-bd98-f0515f895aed
                        resourceVersion: "3564867"
                        uid: 236a1d49-864b-45a2-97ee-b240266c81ad
                    spec:
                        deletionPolicy: Delete
                        forProvider:
                            manifest:
                                apiVersion: v1
                                kind: Namespace
                                metadata:
                                    labels:
                                        example: "true"
                        managementPolicies:
                            - '*'
                        providerConfigRef:
                            name: kubernetes-provider
                        readiness:
                            policy: SuccessfulCreate
                    status:
                        atProvider:
                            manifest:
                                apiVersion: v1
                                kind: Namespace
                                metadata:
                                    annotations:
                                        kubectl.kubernetes.io/last-applied-configuration: '{"apiVersion":"v1","kind":"Namespace","metadata":{"labels":{"example":"true"}}}'
                                    creationTimestamp: "2024-04-16T23:49:48Z"
                                    labels:
                                        example: "true"
                                        kubernetes.io/metadata.name: sample-namespace
                                    managedFields:
                                        - apiVersion: v1
                                          fieldsType: FieldsV1
                                          fieldsV1:
                                            f:metadata:
                                                f:annotations:
                                                    .: {}
                                                    f:kubectl.kubernetes.io/last-applied-configuration: {}
                                                f:labels:
                                                    .: {}
                                                    f:example: {}
                                                    f:kubernetes.io/metadata.name: {}
                                          manager: crossplane-kubernetes-provider
                                          operation: Update
                                          time: "2024-04-16T23:49:48Z"
                                    name: sample-namespace
                                    resourceVersion: "3564851"
                                    uid: 0d24ef45-8ef2-4c81-9c57-08d9634cf39a
                                spec:
                                    finalizers:
                                        - kubernetes
                                status:
                                    phase: Active
                        conditions:
                            - lastTransitionTime: "2024-04-16T23:49:48Z"
                              reason: Available
                              status: "True"
                              type: Ready
                            - lastTransitionTime: "2024-04-16T23:49:48Z"
                              reason: ReconcileSuccess
                              status: "True"
                              type: Synced
        connectiondetails: {}
    test2:
        resource:
            unstructured:
                object:
                    apiVersion: kubernetes.crossplane.io/v1alpha2
                    kind: Object
                    metadata:
                        annotations:
                            crossplane.io/composition-resource-name: test2
                            crossplane.io/external-create-pending: "2024-04-16T23:49:49Z"
                            crossplane.io/external-create-succeeded: "2024-04-16T23:49:49Z"
                            crossplane.io/external-name: sample-namespace-v1
                        creationTimestamp: "2024-04-16T23:49:49Z"
                        finalizers:
                            - finalizer.managedresource.crossplane.io
                        generateName: test1-
                        generation: 2
                        labels:
                            crossplane.io/claim-name: ""
                            crossplane.io/claim-namespace: ""
                            crossplane.io/composite: test1
                        managedFields:
                            - apiVersion: kubernetes.crossplane.io/v1alpha2
                              fieldsType: FieldsV1
                              fieldsV1:
                                f:metadata:
                                    f:annotations:
                                        f:crossplane.io/composition-resource-name: {}
                                    f:finalizers:
                                        v:"finalizer.managedresource.crossplane.io": {}
                                    f:generateName: {}
                                    f:labels:
                                        f:crossplane.io/claim-name: {}
                                        f:crossplane.io/claim-namespace: {}
                                        f:crossplane.io/composite: {}
                                    f:ownerReferences:
                                        k:{"uid":"5dc9b48b-c64d-42b5-bd98-f0515f895aed"}: {}
                                f:spec:
                                    f:deletionPolicy: {}
                                    f:forProvider:
                                        f:manifest:
                                            f:apiVersion: {}
                                            f:kind: {}
                                            f:metadata:
                                                f:labels:
                                                    f:example: {}
                                    f:managementPolicies: {}
                                    f:providerConfigRef:
                                        f:name: {}
                                    f:readiness:
                                        f:policy: {}
                              manager: apiextensions.crossplane.io/composed/e395cc1c249f744c11a49c9cf2815759b904e68321759d7e06b901eed1a97668
                              operation: Apply
                              time: "2024-04-16T23:49:49Z"
                            - apiVersion: kubernetes.crossplane.io/v1alpha2
                              fieldsType: FieldsV1
                              fieldsV1:
                                f:metadata:
                                    f:annotations:
                                        f:crossplane.io/external-create-pending: {}
                                        f:crossplane.io/external-create-succeeded: {}
                                        f:crossplane.io/external-name: {}
                                    f:finalizers:
                                        .: {}
                                        v:"finalizer.managedresource.crossplane.io": {}
                                f:spec:
                                    f:readiness:
                                        .: {}
                                        f:policy: {}
                              manager: crossplane-kubernetes-provider
                              operation: Update
                              time: "2024-04-16T23:49:49Z"
                            - apiVersion: kubernetes.crossplane.io/v1alpha2
                              fieldsType: FieldsV1
                              fieldsV1:
                                f:status:
                                    .: {}
                                    f:atProvider:
                                        .: {}
                                        f:manifest:
                                            .: {}
                                            f:apiVersion: {}
                                            f:kind: {}
                                            f:metadata:
                                                .: {}
                                                f:annotations:
                                                    .: {}
                                                    f:kubectl.kubernetes.io/last-applied-configuration: {}
                                                f:creationTimestamp: {}
                                                f:labels:
                                                    .: {}
                                                    f:example: {}
                                                    f:kubernetes.io/metadata.name: {}
                                                f:managedFields: {}
                                                f:name: {}
                                                f:resourceVersion: {}
                                                f:uid: {}
                                            f:spec:
                                                .: {}
                                                f:finalizers: {}
                                            f:status:
                                                .: {}
                                                f:phase: {}
                                    f:conditions:
                                        .: {}
                                        k:{"type":"Ready"}:
                                            .: {}
                                            f:lastTransitionTime: {}
                                            f:reason: {}
                                            f:status: {}
                                            f:type: {}
                                        k:{"type":"Synced"}:
                                            .: {}
                                            f:lastTransitionTime: {}
                                            f:reason: {}
                                            f:status: {}
                                            f:type: {}
                              manager: crossplane-kubernetes-provider
                              operation: Update
                              subresource: status
                              time: "2024-04-16T23:49:49Z"
                        name: sample-namespace-v1
                        ownerReferences:
                            - apiVersion: crossform.io/v1alpha1
                              blockOwnerDeletion: true
                              controller: true
                              kind: xModule
                              name: test1
                              uid: 5dc9b48b-c64d-42b5-bd98-f0515f895aed
                        resourceVersion: "3564884"
                        uid: d3baddd3-5440-4028-9f1b-4f6638e58d1d
                    spec:
                        deletionPolicy: Delete
                        forProvider:
                            manifest:
                                apiVersion: v1
                                kind: Namespace
                                metadata:
                                    labels:
                                        example: "true"
                        managementPolicies:
                            - '*'
                        providerConfigRef:
                            name: kubernetes-provider
                        readiness:
                            policy: SuccessfulCreate
                    status:
                        atProvider:
                            manifest:
                                apiVersion: v1
                                kind: Namespace
                                metadata:
                                    annotations:
                                        kubectl.kubernetes.io/last-applied-configuration: '{"apiVersion":"v1","kind":"Namespace","metadata":{"labels":{"example":"true"}}}'
                                    creationTimestamp: "2024-04-16T23:49:49Z"
                                    labels:
                                        example: "true"
                                        kubernetes.io/metadata.name: sample-namespace-v1
                                    managedFields:
                                        - apiVersion: v1
                                          fieldsType: FieldsV1
                                          fieldsV1:
                                            f:metadata:
                                                f:annotations:
                                                    .: {}
                                                    f:kubectl.kubernetes.io/last-applied-configuration: {}
                                                f:labels:
                                                    .: {}
                                                    f:example: {}
                                                    f:kubernetes.io/metadata.name: {}
                                          manager: crossplane-kubernetes-provider
                                          operation: Update
                                          time: "2024-04-16T23:49:49Z"
                                    name: sample-namespace-v1
                                    resourceVersion: "3564876"
                                    uid: 5daa43a3-267a-4c40-83c5-c5243262a8f9
                                spec:
                                    finalizers:
                                        - kubernetes
                                status:
                                    phase: Active
                        conditions:
                            - lastTransitionTime: "2024-04-16T23:49:49Z"
                              reason: Available
                              status: "True"
                              type: Ready
                            - lastTransitionTime: "2024-04-16T23:49:49Z"
                              reason: ReconcileSuccess
                              status: "True"
                              type: Synced
        connectiondetails: {}
requested:
    test-request1: []
modulename: test1
xr:
    resource:
        unstructured:
            object:
                apiVersion: crossform.io/v1alpha1
                kind: xModule
                metadata:
                    annotations:
                        kubectl.kubernetes.io/last-applied-configuration: |
                            {"apiVersion":"crossform.io/v1alpha1","kind":"xModule","metadata":{"annotations":{},"name":"test1"},"spec":{"inputs":{"test1":"aaa"},"path":"project1","repository":"git@github.com:zefir01/test2.git","revision":"main"}}
                    creationTimestamp: "2024-04-16T23:49:46Z"
                    finalizers:
                        - composite.apiextensions.crossplane.io
                    generation: 5
                    labels:
                        crossplane.io/composite: test1
                    managedFields:
                        - apiVersion: crossform.io/v1alpha1
                          fieldsType: FieldsV1
                          fieldsV1:
                            f:spec:
                                f:resourceRefs: {}
                          manager: apiextensions.crossplane.io/composite
                          operation: Apply
                          time: "2024-04-16T23:49:49Z"
                        - apiVersion: crossform.io/v1alpha1
                          fieldsType: FieldsV1
                          fieldsV1:
                            f:status:
                                f:conditions:
                                    k:{"type":"Ready"}:
                                        .: {}
                                        f:lastTransitionTime: {}
                                        f:reason: {}
                                        f:status: {}
                                        f:type: {}
                                    k:{"type":"Synced"}:
                                        .: {}
                                        f:lastTransitionTime: {}
                                        f:reason: {}
                                        f:status: {}
                                        f:type: {}
                                f:hasErrors: {}
                                f:outputs:
                                    f:test1: {}
                                f:report:
                                    f:inputs:
                                        f:test1: {}
                                    f:inputsValidation: {}
                                    f:outputs:
                                        f:test1: {}
                                    f:requests:
                                        f:test-request1: {}
                                    f:resources:
                                        f:test1: {}
                                        f:test2: {}
                                f:repository:
                                    f:commitSha: {}
                                    f:message: {}
                                    f:ok: {}
                          manager: apiextensions.crossplane.io/composite
                          operation: Apply
                          subresource: status
                          time: "2024-04-18T00:51:28Z"
                        - apiVersion: crossform.io/v1alpha1
                          fieldsType: FieldsV1
                          fieldsV1:
                            f:metadata:
                                f:finalizers:
                                    .: {}
                                    v:"composite.apiextensions.crossplane.io": {}
                                f:labels:
                                    .: {}
                                    f:crossplane.io/composite: {}
                            f:spec:
                                f:compositionRef:
                                    .: {}
                                    f:name: {}
                                f:compositionRevisionRef:
                                    .: {}
                                    f:name: {}
                          manager: crossplane
                          operation: Update
                          time: "2024-04-16T23:49:46Z"
                        - apiVersion: crossform.io/v1alpha1
                          fieldsType: FieldsV1
                          fieldsV1:
                            f:metadata:
                                f:annotations:
                                    .: {}
                                    f:kubectl.kubernetes.io/last-applied-configuration: {}
                            f:spec:
                                .: {}
                                f:compositionUpdatePolicy: {}
                                f:inputs:
                                    .: {}
                                    f:test1: {}
                                f:path: {}
                                f:repoServer: {}
                                f:repository: {}
                                f:revision: {}
                          manager: kubectl-client-side-apply
                          operation: Update
                          time: "2024-04-16T23:49:46Z"
                        - apiVersion: crossform.io/v1alpha1
                          fieldsType: FieldsV1
                          fieldsV1:
                            f:status:
                                f:conditions:
                                    .: {}
                                    k:{"type":"Ready"}:
                                        .: {}
                                        f:lastTransitionTime: {}
                                        f:reason: {}
                                        f:status: {}
                                        f:type: {}
                                    k:{"type":"Synced"}:
                                        .: {}
                                        f:lastTransitionTime: {}
                                        f:reason: {}
                                        f:status: {}
                                        f:type: {}
                          manager: crossplane
                          operation: Update
                          subresource: status
                          time: "2024-04-19T00:13:41Z"
                    name: test1
                    resourceVersion: "3878611"
                    uid: 5dc9b48b-c64d-42b5-bd98-f0515f895aed
                spec:
                    compositionRef:
                        name: crossform-default
                    compositionRevisionRef:
                        name: crossform-default-0d8ff14
                    compositionUpdatePolicy: Automatic
                    inputs:
                        test1: aaa
                    path: project1
                    repoServer: 192.168.1.173:8083
                    repository: git@github.com:zefir01/test2.git
                    resourceRefs:
                        - apiVersion: kubernetes.crossplane.io/v1alpha2
                          kind: Object
                          name: sample-namespace
                        - apiVersion: kubernetes.crossplane.io/v1alpha2
                          kind: Object
                          name: sample-namespace-v1
                    revision: main
                status:
                    conditions:
                        - lastTransitionTime: "2024-04-19T00:13:41Z"
                          reason: ReconcileSuccess
                          status: "True"
                          type: Synced
                        - lastTransitionTime: "2024-04-16T23:49:49Z"
                          reason: Available
                          status: "True"
                          type: Ready
                    hasErrors: false
                    outputs:
                        test1: aaa
                    report:
                        inputs:
                            test1: OK
                        inputsValidation: OK
                        outputs:
                            test1: OK
                        requests:
                            test-request1: OK
                        resources:
                            test1: OK
                            test2: OK
                    repository:
                        commitSha: 228a78fbf0008aa50900198a48ccd9fcbd9d132d
                        message: No updates
                        ok: true
    connectiondetails: {}
context: '{"apiextensions.crossplane.io/environment":{"apiVersion":"internal.crossplane.io/v1alpha1", "kind":"Environment"}}'

output:

user@ws:~/GolandProjects/nickel-test$ nickel eval main.ncl 
error: incompatible types
   ┌─ /home/user/GolandProjects/nickel-test/crossform.ncl:17:87
   │
17 │           ready: Bool = std.array.any (fun x => x.type=="Ready" && x.status=="True") (getConditions _id) && std.array.any (fun x => x.type=="Synced" && x.status=="True") (getConditions _id),
   │                                                                                       ^^^^^^^^^^^^^ this expression
   │
   = Expected an expression of type `_a -> _b`
   = Found an expression of type `Dyn`
   = These types are not compatible

Environment

yannham commented 2 months ago

Hello,

The issue you're seeing is that the typechecker is expecting a function for getConditions, but it can only deduce that it has type Dyn.

When using a type annotation ready : Bool, the static typechecker kicks in - as in any general purpose programming language out there. The static typechecker is strict by nature, and beside a few exceptions, anything that isn't typed is considered Dyn - thus, because getConditions and getResources aren't annotated, they aren't typechecked, and the typechecker doesn't know their types.

In other words, static typing is "contagious": if you function f from within a statically typed function g, then f usually needs to be typed as well.

Usually, my general advice in this case would thus be to add the missing type annotation on getResources and getConditions.

However, in your case, it's not gonna be very easy, because cmd is dynamically imported, and you're dynamically looking for some fields in it, and using a contract Resource in the mix, which isn't very easy to handle with static typing.

Solutions

  1. The simplest one, although the less "safe" (the one with less safety checks), is to turn your type annotation into a contract annotation. Instead of being checked statically by the typechecker, it will be checked at runtime (that the input is a String and the return value is a Resource), and the typechecker doesn't verify the content of the function anymore.

  2. Another solution is to keep _resource typed, and push the contract annotations to getConditions and getResources. This requires to come up with slightly more elaborate types. The benefit is that more stuff is checked; in particular _resource is still statically typechecked.

Here is a working example of 2.:

let cmd = import "command.yaml" in

let Resource = {
  _crossform = {
    id | String,
    ttt | String = "ggg",
  },
  ..
}
in

let getResource
  | String -> Array { .. }
  = fun _id =>
    if std.record.has_field _id cmd.observed then
      cmd.observed."%{_id}".resource.unstructured.object
    else
      {}
  in

let getConditions | String -> Array {type: String, status: String; Dyn} = fun _id =>
  getResource _id |> match {
    { status={conditions, ..}, .. } => conditions,
    _ => []
  }
in

let _resource : String -> Resource = fun _id =>
    {
      _crossform = {
        id : String = _id,
        ready : Bool =
            std.array.any
              (
                fun x =>
                  x.type == "Ready"
                  && x.status == "True"
              )
              (getConditions _id)
            && std.array.any
              (
                fun x =>
                  x.type == "Synced" && x.status == "True"
              )
              (getConditions _id),
        #test=getConditions _id
      }
    }
    & getResource _id | Resource
  in

{
  resource = _resource,
}

The type {type: String, status: String; Dyn} represents record that have at least fields type and status, but might have more fields as well. I don't know what your actual data look like; if in command.yaml there are never other fields than type and status, you can get rid of the ; Dyn part.

Note that, beside reformatting, I also had to fix some errors: you missed parentheses around getConditions _id in the calls to any. As you wrote it, it's like you're passing 3 arguments to any: the function, another function getConditions, and at last a String _id. This was caught by typechecker after annotating getResources and getConditions, which is a good illustration why approach 2 is safer!

Additionally, It's not an error, but I've simplified your two match statements to only one: you can actually nest patterns in a match.

In general, I would advise to add more parentheses when things aren't obvious: for example, I find that the line

    & getResource _id  | Resource

might make it look like the Resource contract is applied only to getResource _id, while actually it's applied to the whole merge expression. I would add parentheses to make this clear (but this is only a matter of taste here, not correctness, as | has lower precedence than most operators).

Also, feel free to improve the annotation of getResources or getConditions if you know more precise types for them!

zefir01 commented 2 months ago

thank you very much. this solves the problem.